Podstawy PHP w pigułce: Tablice
Jedna struktura zamiast kilku
W większości języków lista, mapa i zbiór to trzy osobne typy: w Javie List, Map i Set; w C# List,
Dictionary i HashSet; w JavaScripcie i TypeScripcie Array, Map i Set. PHP zaskoczy Cię prostotą i zarazem
niekonsekwencją: jeden typ array zastępuje je wszystkie, a do tego pełni rolę stosu i kolejki.
Technicznie tablica PHP to uporządkowany hash map (po polsku: uporządkowana mapa haszująca). Pamięta kolejność wstawiania, a jednocześnie mapuje
klucze na wartości. Nie ma osobnego typu listy — „lista” to po prostu tablica, która akurat ma klucze 0, 1, 2, …
bez dziur.
$indexed = [1, 2, 3]; // klucze 0, 1, 2$assoc = ['name' => 'Paweł', 'age' => 30];$nested = ['user' => ['roles' => ['admin']]];
echo $nested['user']['roles'][0]; // 'admin'To jedno zdanie — „uporządkowany hash map” — wyjaśnia większość dziwnych zachowań, które zobaczysz niżej. Kiedy coś Cię zaskoczy, wróć do niego.
Hash map to struktura trzymająca pary klucz → wartość, która potrafi znaleźć wartość po kluczu bez przeglądania całej zawartości. Klucz jest przeliczany na liczbę (skrót, hash), a ta wskazuje od razu miejsce, w którym wartość leży — dlatego
$a['name']kosztuje tyle samo przy dziesięciu i przy milionie elementów. Klasyczny hash map nie obiecuje przy tym żadnej kolejności elementów. PHP obiecuje — i właśnie dlatego „uporządkowany”.
Klucze
Dozwolone są tylko int i string
Wszystko inne jest po cichu rzutowane:
| Podany klucz | Rzeczywisty klucz |
|---|---|
'8' |
8 — string liczbowy staje się intem |
'08' |
'08' — wiodące zero, więc zostaje stringiem |
8.7 |
8 — część ułamkowa ucięta |
true / false |
1 / 0 |
null |
'' — pusty string |
| obiekt / tablica | TypeError |
Pierwszy wiersz to najczęstsze źródło zdziwienia. $a['8'] i $a[8] to ten sam klucz, więc tablica
z danych wejściowych potrafi po cichu scalić dwa pola:
$a = [];$a['8'] = 'pierwszy';$a[8] = 'drugi';
count($a); // 1 — nie 2$a[8]; // 'drugi'Zwróć uwagę na '08': tylko kanoniczny zapis liczby całkowitej jest konwertowany. '08', '8.0', ' 8'
i '+8' zostają stringami.
Dopisywanie i usuwanie
$a = [];$a[] = 'x'; // klucz 0$a[10] = 'y';$a[] = 'z'; // klucz 11 — najwyższy klucz CAŁKOWITY + 1, nie count()
unset($a[0]); // usuwa klucz, NIE przenumerowuje reszty$a = array_values($a); // dopiero to przenumerowuje na 0..nReguła „najwyższy dotychczasowy klucz całkowity plus jeden” obowiązuje nawet po usunięciu elementu — licznik nie
cofa się. To dlatego tablica po unset() albo array_filter() ma dziury i przestaje być listą.
$a = [1, 2, 3];unset($a[1]);
$a; // [0 => 1, 2 => 3] — dziura na pozycji 1array_is_list($a); // false (PHP 8.1)json_encode($a); // {"0":1,"2":3} — OBIEKT, nie tablica JSONarray_is_list(array_values($a)); // trueOstatnia linijka to realny problem w API. Dopóki tablica jest listą, json_encode() produkuje […]. Jedna dziura
w kluczach i ten sam kod zaczyna zwracać {…} — a klient po drugiej stronie się wywala. Po każdym
array_filter() na liście używaj array_values().
Rozpakowywanie i destrukturyzacja
Operator ...
// klucze tekstowe: przy kolizji wygrywa ten z prawej$defaults = ['a' => 1];$options = ['a' => 9, 'b' => 2];
[...$defaults, ...$options]; // ['a' => 9, 'b' => 2] — 'a' => 1 przepadło
// klucze całkowite: przepisywane od zera, więc kolizja nikogo nie zjada$x = [5 => 'a', 9 => 'b'];$y = [5 => 'c'];
[...$x, ...$y]; // [0 => 'a', 1 => 'b', 2 => 'c'] // klucze 5 i 9 zniknęły, ale wszystkie trzy wartości zostałyRozpakowywanie działa od PHP 7.4 dla kluczy całkowitych, a dla kluczy tekstowych dopiero od 8.1. Różnica między tymi dwoma przypadkami jest istotna: klucz tekstowy przy kolizji nadpisuje wartość, a klucz całkowity nie może kolidować, bo i tak zostanie przepisany na kolejny numer od zera.
Destrukturyzacja po kluczach
$user = ['name' => 'Paweł', 'age' => 30];
['name' => $n, 'age' => $g] = $user; // $n = 'Paweł', $g = 30Kolejność w takim wzorcu nie ma znaczenia — liczą się nazwy. To wariant, po który sięgasz najczęściej przy danych z bazy albo z requestu.
Destrukturyzacja pozycyjna
$row = [1, 2, 3];
[$first, , $third] = $row; // podwójny przecinek POMIJA element // $first = 1, $third = 3Prosto w foreach
To jest zastosowanie, które realnie skraca kod:
$rows = [ ['id' => 1, 'name' => 'A'], ['id' => 2, 'name' => 'B'],];
foreach ($rows as ['id' => $id, 'name' => $name]) { echo "$id: $name";}
foreach ([[1, 2], [3, 4]] as [$left, $right]) { } // pozycyjnie też działaZagnieżdżenia i zamiana bez zmiennej pomocniczej
[[$x, $y], [$z]] = [[1, 2], [3]]; // $x=1, $y=2, $z=3['a' => ['b' => $deep]] = ['a' => ['b' => 'jest']]; // $deep = 'jest'
$arr = [1, 2, 3];[$arr[2], $arr[0]] = [$arr[0], $arr[2]]; // [3, 2, 1]Brakujący klucz we wzorcu to ostrzeżenie, nie błąd — zmienna dostaje
null.['x' => $x] = ['y' => 1];daje$x === null. Nie ma składni na wartość domyślną, więc gdy klucz jest opcjonalny, napisz zwyczajnie$x = $arr['x'] ?? 'domyślna';.
list($a, $b) = $arr; to stary zapis tego samego. W nowym kodzie używaj nawiasów kwadratowych.
Funkcje array_*
Jest ich ponad osiemdziesiąt, a poniżej te, które najczęściej spotkasz w codziennej pracy.
$a = [3, 1, 2];$assoc = ['a' => 1, 'b' => 2];
// ── Mapowanie, filtrowanie, redukcja ─────────────────────────────────────────array_map(fn($v) => $v * 2, $a); // [6, 2, 4]array_map(fn($x, $y) => $x + $y, [1, 2], [10, 20]); // [11, 22] — callback dostaje po jednym // elemencie z KAŻDEJ tablicyarray_map(null, [1, 2], ['x', 'y']); // [[1, 'x'], [2, 'y']] — bez callbacku łączy w paryarray_filter($a, fn($v) => $v > 1); // [0 => 3, 2 => 2] ← klucze z dziurąarray_filter($assoc, fn($k) => $k === 'a', ARRAY_FILTER_USE_KEY); // ['a' => 1]array_reduce($a, fn($carry, $v) => $carry + $v, 0); // 6
// ── Klucze i wartości ────────────────────────────────────────────────────────array_keys($assoc); // ['a', 'b']array_values($assoc); // [1, 2]array_key_first($assoc); // 'a'array_key_last($assoc); // 'b'
// ── Czy coś tam jest ─────────────────────────────────────────────────────────in_array(3, $a, true); // true — szuka WARTOŚCIarray_search(3, $a, true); // 0 — to samo, ale zwraca KLUCZarray_key_exists('a', $assoc); // true — szuka KLUCZA, nie wartości
// trzeci argument to porównanie ŚCISŁE (===); bez niego działa luźne (==)in_array('3', $a); // true — '3' == 3in_array('3', $a, true); // false — '3' !== 3, bo string kontra int
// array_key_exists() odróżnia „klucza nie ma" od „klucz jest, ale wartość to null"$withNull = ['a' => null];array_key_exists('a', $withNull); // true — klucz istniejeisset($withNull['a']); // false — isset() traktuje null jak brak
// ── Łączenie ─────────────────────────────────────────────────────────────────array_merge(['a' => 1], ['a' => 2, 'b' => 3]); // ['a' => 2, 'b' => 3] — prawa wygrywa['a' => 1] + ['a' => 2, 'b' => 3]; // ['a' => 1, 'b' => 3] — lewa wygrywaarray_combine(['x', 'y'], [10, 20]); // ['x' => 10, 'y' => 20]
// ── Wycinanie i dzielenie ────────────────────────────────────────────────────array_slice([1, 2, 3, 4, 5], 1, 3); // [2, 3, 4]array_chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]range('a', 'e'); // ['a', 'b', 'c', 'd', 'e']
// ── Wiersze i kolumny ────────────────────────────────────────────────────────$rows = [['id' => 7, 'name' => 'A'], ['id' => 9, 'name' => 'B']];array_column($rows, 'name'); // ['A', 'B']array_column($rows, 'name', 'id'); // [7 => 'A', 9 => 'B']
// ── Agregacje ────────────────────────────────────────────────────────────────array_sum($a); // 6 — suma wartości: 3 + 1 + 2array_product($a); // 6 — iloczyn wartości: 3 * 1 * 2count($a); // 3 — liczba elementów
// ── Stos i kolejka ───────────────────────────────────────────────────────────array_push($a, 4); // dodaje na KOŃCUarray_pop($a); // usuwa z KOŃCA i zwraca usuniętą wartośćarray_unshift($a, 0); // dodaje na POCZĄTKUarray_shift($a); // usuwa z POCZĄTKU i zwraca usuniętą wartość
// ── Różnice i przecięcia ─────────────────────────────────────────────────────array_diff([1, 2, 3], [2]); // [0 => 1, 2 => 3] — co jest w lewej, a nie w prawejarray_intersect([1, 2, 3], [2, 3, 4]); // [1 => 2, 2 => 3] — co jest w obuarray_diff_key($assoc, ['a' => 0]); // ['b' => 2] — porównuje KLUCZE, nie wartości
// ── Unikalne i odwrócenie ────────────────────────────────────────────────────array_unique([1, '1', 2]); // [0 => 1, 2 => 2] — porównuje jako STRINGIarray_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b'] — zamienia klucze z wartościamiDwie rzeczy z sekcji o mapowaniu warto rozwinąć. Po pierwsze, array_map() przyjmuje dowolnie wiele tablic —
wtedy callback dostaje po jednym elemencie z każdej, brane po kolei z tych samych pozycji. Po drugie, podanie
null zamiast callbacku sprawia, że PHP niczego nie przekształca, tylko zestawia te tablice w pary — czyli
robi to, co w innych językach nazywa się zip.
Trzy, które warto znać dobrze
array_column() zamienia wynik zapytania w mapę i oszczędza pętli:
$rows = [ ['id' => 7, 'name' => 'A'], ['id' => 9, 'name' => 'B'],];
array_column($rows, 'name'); // ['A', 'B']array_column($rows, 'name', 'id'); // [7 => 'A', 9 => 'B']array_column($rows, null, 'id'); // [7 => […], 9 => […]] — całe wiersze po idarray_filter() bez callbacku usuwa wszystko, co fałszywe — czyli także 0 i '0':
array_filter([0, 1, '', '0', 'a', null]); // [1 => 1, 4 => 'a']Jeśli chcesz wyrzucić tylko null, powiedz to wprost:
array_filter($a, fn($v) => $v !== null);in_array() domyślnie porównuje luźno — a wiesz już z poprzedniej części, co to oznacza:
in_array('abc', [0, 1]); // false w PHP 8 (w PHP 7 było true!)in_array(0, ['a', 'b']); // false w PHP 8in_array('1', [1, 2]); // true — '1' i 1 są luźno równe
in_array('1', [1, 2], true); // false — trzeci argument włącza ===Zawsze podawaj
truejako trzeci argumentin_array(),array_search()iarray_keys(). Zmiana z PHP 8 usunęła najgorsze przypadki, ale luźne porównanie nadal potrafi zaskoczyć.
Pozostałe pułapki
$a[-1]; // w TABLICY to po prostu klucz -1, nie indeksowanie od końca$str[-1]; // ale STRING indeksuje się od końca — to ostatni znak
array_push($a, 1, 2); // przez referencję, wiele wartości naraz$a[] = 1; // dla jednej wartości szybsze i czytelniejsze
array_reduce($a, $cb); // bez trzeciego argumentu akumulator startuje z NULLarray_diff_assoc($a, $b); // porównuje KLUCZ i wartość, nie samą wartośćarray_unique($a); // domyślnie porównuje po rzutowaniu na STRINGPierwszy argument callbacku w array_reduce() to akumulator — wartość przenoszona z obiegu na obieg,
w dokumentacji PHP nazwana $carry. Bez trzeciego argumentu zaczyna od null, więc przy dodawaniu liczb warto
podać 0 jawnie.
Różnicę między array_diff() a array_diff_assoc() najlepiej widać na przykładzie — pierwsza patrzy tylko na
wartości, druga wymaga zgodności pary klucz-wartość:
$a = ['x' => 1, 'y' => 2];$b = ['z' => 1];
array_diff($a, $b); // ['y' => 2] — 1 jest w $b, więc wypadaarray_diff_assoc($a, $b); // ['x' => 1, 'y' => 2] — para 'x' => 1 w $b nie istniejearray_unique() też zasługuje na przykład. Domyślnie zamienia wartości na stringi przed porównaniem, więc 1,
'1' i true wyglądają dla niej identycznie:
array_unique([1, '1', true]); // [0 => 1] — zostaje jeden elementarray_unique([1, '1', true], SORT_REGULAR); // [0 => 1, 2 => true] — porównuje bez rzutowaniaKoszt operacji na początku tablicy
array_shift() i array_unshift() przenumerowują wszystkie klucze całkowite, więc kosztują O(n) — przy
tablicy w pętli to O(n²). array_push() i array_pop() działają na końcu i są O(1).
Jeśli nie znasz zapisu
O(…), to notacja dużego O — sposób opisywania, jak rośnie koszt operacji wraz z liczbą elementów. Warto kiedyś doczytać, a na teraz wystarczy jedna intuicja:O(1)znaczy „koszt stały, niezależny od rozmiaru”, aO(n)— „koszt rośnie wprost proporcjonalnie do liczby elementów”. Czyli to drugie jest wyraźnie droższe od pierwszego, aO(n²)jeszcze wyraźniej.
// ❌ O(n²) na dużej tablicywhile ($item = array_shift($queue)) { }
// ✅ liniowoforeach ($queue as $item) { }Sortowanie
W PHP funkcje sortujące mutują tablicę w miejscu i zwracają bool, a nie posortowaną tablicę. To pierwsza
rzecz, która myli osoby przychodzące z innych języków:
$sorted = sort($a); // ❌ $sorted to true, a $a zostało zmodyfikowanesort($a); // ✅ tak się tego używaNazwy wyglądają chaotycznie, ale mają system: przedrostek a oznacza zachowanie kluczy (associative), k
sortowanie po kluczu (key), u własny callback (user), r kolejność malejącą (reverse).
| Funkcja | Sortuje po | Zachowuje klucze |
|---|---|---|
sort() / rsort() |
wartości | ❌ przenumerowuje |
asort() / arsort() |
wartości | ✅ |
ksort() / krsort() |
kluczu | ✅ |
usort() |
własnym callbacku, po wartości | ❌ |
uasort() |
własnym callbacku, po wartości | ✅ |
uksort() |
własnym callbacku, po kluczu | ✅ |
natsort() / natcasesort() |
porządku naturalnym (img12 po img2) |
✅ |
array_multisort() |
wielu tablicach lub kolumnach | ❌ |
Callback zwraca liczbę ujemną, zero albo dodatnią — czyli dokładnie to, co daje operator <=> z poprzedniej
części:
usort($users, fn($a, $b) => $a->age <=> $b->age); // rosnąco
usort($users, fn($a, $b) => [$a->last, $a->first] // po wielu polach naraz <=> [$b->last, $b->first]);Drugi przykład wykorzystuje to, że <=> porównuje tablice element po elemencie — dostajesz sortowanie po
nazwisku, a przy remisie po imieniu, bez pisania if-ów.
Od PHP 8.0 sortowanie jest stabilne: elementy równe zachowują pierwotną kolejność. Wcześniej kolejność była nieokreślona, więc kod, który sortował dwa razy po różnych polach, dawał różne wyniki na różnych wersjach.
array_merge() kontra operator +
To jest ta różnica, na której wykłada się najwięcej osób, więc zostawiłem ją na koniec.
array_merge(['a' => 1, 'b' => 2], ['b' => 3]); // ['a'=>1, 'b'=>3] — PRAWA wygrywa['a' => 1, 'b' => 2] + ['b' => 3, 'c' => 4]; // ['a'=>1, 'b'=>2, 'c'=>4] — LEWA wygrywa
array_merge([1, 2], [3]); // [1, 2, 3] — klucze całkowite PRZENUMEROWANE[1, 2] + [3]; // [1, 2] — kolizja klucza 0, prawa strona odrzuconaDwie reguły, które wystarczy zapamiętać:
array_merge()— klucze tekstowe nadpisuje prawa strona, klucze całkowite są przenumerowywane i nic nie ginie.+— pierwszeństwo ma lewa strona i żaden klucz nie jest zmieniany, więc kolidujące elementy z prawej po prostu znikają.
Stąd praktyczne zastosowania: + służy do uzupełniania wartościami domyślnymi, a nie do sklejania list.
$config = $userConfig + $defaults; // ustawienia użytkownika mają pierwszeństwoDo sklejania list używaj array_merge() albo rozpakowywania [...$a, ...$b] — oba przenumerują klucze
i nie zgubią elementów.
Co dalej
Znasz już wartości, typy i strukturę, która je przechowuje. Pora na to, co się z nimi robi.
W następnej części zajmiemy się operatorami — łącznie z pułapką priorytetów, przez którą and i or
nie są zamiennikami && i || — oraz sterowaniem przepływem: pętlami, switch kontra match i tym, czym
naprawdę różni się require od include.