deftcode
English
← Powrót do bloga

Podstawy PHP w pigułce: Tablice

Przeczytaj po angielsku
phpkurs phptablice

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

Reguł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 1
array_is_list($a); // false (PHP 8.1)
json_encode($a); // {"0":1,"2":3} — OBIEKT, nie tablica JSON
array_is_list(array_values($a)); // true

Ostatnia 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ły

Rozpakowywanie 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 = 30

Kolejność 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 = 3

Prosto 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ła

Zagnież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 tablicy
array_map(null, [1, 2], ['x', 'y']); // [[1, 'x'], [2, 'y']] — bez callbacku łączy w pary
array_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ŚCI
array_search(3, $a, true); // 0 — to samo, ale zwraca KLUCZ
array_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' == 3
in_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 istnieje
isset($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 wygrywa
array_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 + 2
array_product($a); // 6 — iloczyn wartości: 3 * 1 * 2
count($a); // 3 — liczba elementów
// ── Stos i kolejka ───────────────────────────────────────────────────────────
array_push($a, 4); // dodaje na KOŃCU
array_pop($a); // usuwa z KOŃCA i zwraca usuniętą wartość
array_unshift($a, 0); // dodaje na POCZĄTKU
array_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 prawej
array_intersect([1, 2, 3], [2, 3, 4]); // [1 => 2, 2 => 3] — co jest w obu
array_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 STRINGI
array_flip(['a' => 1, 'b' => 2]); // [1 => 'a', 2 => 'b'] — zamienia klucze z wartościami

Dwie 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 id

array_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 8
in_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 true jako trzeci argument in_array(), array_search() i array_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 NULL
array_diff_assoc($a, $b); // porównuje KLUCZ i wartość, nie samą wartość
array_unique($a); // domyślnie porównuje po rzutowaniu na STRING

Pierwszy 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 wypada
array_diff_assoc($a, $b); // ['x' => 1, 'y' => 2] — para 'x' => 1 w $b nie istnieje

array_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 element
array_unique([1, '1', true], SORT_REGULAR); // [0 => 1, 2 => true] — porównuje bez rzutowania

Koszt 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”, a O(n) — „koszt rośnie wprost proporcjonalnie do liczby elementów”. Czyli to drugie jest wyraźnie droższe od pierwszego, a O(n²) jeszcze wyraźniej.

// ❌ O(n²) na dużej tablicy
while ($item = array_shift($queue)) { }
// ✅ liniowo
foreach ($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 zmodyfikowane
sort($a); // ✅ tak się tego używa

Nazwy 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 odrzucona
Diagram: array_merge nadpisuje kolidujące klucze wartościami z prawej tablicy i przenumerowuje klucze całkowite, operator plus zachowuje wartości z lewej
Przy kolizji klucza array_merge() bierze wartość z prawej, operator + zostawia lewą.

Dwie 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ństwo

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

Komentarze