deftcode
English
← Powrót do bloga

Podstawy PHP w pigułce: Funkcje i closures

Przeczytaj po angielsku
phpkurs phpfunkcje

O czym jest ta część

Definiowanie funkcji w PHP jest w dużej mierze bardzo podobne do definiowania ich w innych językach programowania. Są jednak rzeczy, które dla osób przychodzących z takich języków jak JavaScript czy Python mogą być zaskakujące: mianowicie fakt, że w PHP funkcja nie widzi zmiennych ze swojego otoczenia, oraz to, że nazwy parametrów są częścią jej publicznego API (dzięki możliwości wykorzystania tzw. argumentów nazwanych, o czym niżej).

W tym artykule omówimy sobie te niespodzianki, o których wspominam wyżej, oraz przejdziemy przez typy w sygnaturze, argumenty nazwane i wariadyczne, zasięg zmiennych, domknięcia i callable.


Deklaracja funkcji

function greet(string $name, string $greeting = 'Cześć'): string
{
return "$greeting, $name!";
}

Typy parametrów i typ zwracany są opcjonalne składniowo, ale dobrą praktyką jest zawsze je definiować. Bez nich declare(strict_types=1) z pierwszej części nie ma czego pilnować.

Typy złożone

function find(?int $id): ?User { } // ?T to skrót od T|null — czyli to samo co: find(int|null $id): User|null

Znak zapytania przed typem znaczy „albo null“. Zapis bez niego wygląda tak samo, tylko jest dłuższy — dla PHP ?int i int|null to dokładnie ten sam typ.

function id(int|string $id): int|string { } // unia (PHP 8.0) — int ALBO string
function render(Stringable&Countable $x): void { } // przecięcie (PHP 8.1) — Stringable I Countable naraz

Unia oznacza „jedno z”, przecięcie „wszystkie naraz”. Przecięcia działają tylko na typach klasowych — nie da się napisać int&string, bo to i tak byłoby niemożliwe do spełnienia.

Skoro przecięcie wymaga typów klasowych, przykład domaga się klasy:

class Basket implements Stringable, Countable
{
public function __toString(): string { return 'Koszyk'; } // wymagane przez Stringable
public function count(): int { return 3; } // wymagane przez Countable
}
render(new Basket()); // OK — Basket spełnia oba interfejsy naraz
render('koszyk'); // TypeError — string nie jest ani jednym, ani drugim

Klasy i interfejsy to temat części siódmej, więc na razie nie musisz rozumieć, jakie zadanie pełni tutaj implements (chociaż jeśli masz doświadczenie w innym języku programowania wspierającym klasy, pewnie doskonale wiesz, czemu to służy). Na razie zapamiętaj, że w kodzie wyżej Stringable&Countable przyjmie wyłącznie taki obiekt, który spełnia oba warunki jednocześnie.

Specjalne typy zwracane

function log(string $m): void { } // void — funkcja nic nie zwraca, po prostu się wykonuje
function fail(): never { throw new … ; } // never (PHP 8.1) — funkcja NIGDY się nie kończy

Różnica między void a never jest istotna: funkcja void wykonuje się do końca i wraca do miejsca wywołania, tylko bez wartości. Funkcja never NIGDY się nie kończy — rzuca wyjątek albo wywołuje exit. Dzięki temu analiza statyczna wie, że kod po jej wywołaniu jest nieosiągalny:

function abort(string $msg): never
{
throw new RuntimeException($msg);
}
$user = $repo->find($id) ?? abort('Nie ma takiego użytkownika');
// do tej linii docieramy TYLKO wtedy, gdy abort() się nie wykonało,
// czyli gdy find() coś zwróciło — więc $user na pewno nie jest nullem

Skoro abort() deklaruje never, analizator wie, że prawa strona ?? nigdy nie zwróci wartości — jedyne wyjście z tej linii w dół prowadzi przez niepusty wynik find(). Stąd pewność, że $user nie jest nullem: gdyby był, wykonanie nie doszłoby do tego miejsca, tylko poleciałoby wyjątkiem.

Jest jeszcze trzeci taki typ — static (PHP 8.0) — ale sens ma tylko wewnątrz klasy:

class Model
{
public static function make(): static // „instancja TEJ klasy, na której wywołano metodę"
{
return new static();
}
}
class User extends Model { }
$u = User::make(); // typ zwracany to User, a nie Model

Gdyby make() deklarowało : Model, analizator wiedziałby tylko tyle, że dostajesz jakiś Model. Dziedziczenie omawiamy w części siódmej — na razie wystarczy zapamiętać, że static jako typ zwracany oznacza „ten sam typ, na którym wywołano metodę”.


Parametry

Wartości domyślne

function paginate(int $page, int $perPage = 20) { }

Wartość domyślna musi być wyrażeniem stałym — literałem, stałą, tablicą literałów, ewentualnie wyrażeniem zbudowanym z takich elementów. Nie może to być wynik wywołania funkcji.

const VAT = 0.23;
function price(float $net, float $rate = 1 + VAT) { } // OK — wyrażenie zbudowane ze stałej i literału, policzone na etapie kompilacji
function bad(int $x = random_int(1, 10)) { } // błąd — wywołanie funkcji

Od PHP 8.1 wyjątkiem jest new w miejscu wartości domyślnej — powiemy sobie o tym więcej w części dziewiątej całej serii.

Parametry z wartością domyślną powinny stać na końcu listy. Postawienie ich wcześniej jest dozwolone, ale zmusza wywołującego do podania wszystkiego i tak — chyba że użyje argumentów nazwanych.

Argumenty nazwane

htmlspecialchars('<b>Cześć</b>', double_encode: false); // nazwa parametru wzięta wprost z sygnatury funkcji

Nazwy nie bierzesz znikąd — to dokładnie te nazwy, które autor funkcji wpisał w jej sygnaturze. Widać to najlepiej na własnym przykładzie:

function paginate(int $page, int $perPage = 20, string $sort = 'id', bool $desc = false): string
{
// …
}
paginate(2, 20, 'id', true); // co oznacza to 20 i to true?
paginate(page: 2, desc: true); // te same argumenty, tylko czytelne — i bez powtarzania domyślnych

Argumenty nazwane (PHP 8.0) rozwiązują dwa problemy naraz: pozwalają pominąć środkowe parametry domyślne i sprawiają, że wywołanie samo się dokumentuje. setVisible(true, false, true) mało mówi na temat wartości przekazanych do tej funkcji, natomiast setVisible(header: true, footer: false, sidebar: true) pozwala łatwo zrozumieć, z czym mamy do czynienia, bez zaglądania do kodu funkcji.

Nazwa parametru staje się częścią Twojego publicznego API. Zmiana nazwy parametru w kolejnej wersji biblioteki to zmiana łamiąca kompatybilność wsteczną — dokładnie tak samo jak zmiana jego typu. Ta sama pułapka czeka przy nadpisywaniu metod: jeśli w klasie potomnej nazwiesz parametr inaczej niż w klasie bazowej, to samo wywołanie z argumentem nazwanym zadziała dla jednej klasy, a wywali się dla drugiej — mimo że obie mają „tę samą” metodę.

Parametry wariadyczne

function sum(int ...$nums): int
{
return array_sum($nums); // $nums to zwykła tablica
}
sum(1, 2, 3); // 6

Trzy kropki przed parametrem zbierają wszystkie pozostałe argumenty do tablicy. Typ przed ... obowiązuje każdy z nich z osobna.

W praktyce znaczy to, że wewnątrz funkcji $nums jest tablicą samych intów. PHP nie potrafi tego wyrazić w sygnaturze — array to po prostu array, bez informacji o zawartości. Zapisuje się to w docblocku: @param int[] $nums, albo list<int>, jeśli używasz PHPStana lub Psalma.

W miejscu wywołania te same trzy kropki działają odwrotnie — rozpakowują tablicę na pojedyncze argumenty:

$args = [1, 2, 3];
sum(...$args); // to samo co sum(1, 2, 3)
paginate(...['page' => 2, 'perPage' => 50]); // to samo co paginate(page: 2, perPage: 50)

Klucze tekstowe w rozpakowywanej tablicy działają jak argumenty nazwane (PHP 8.1).

Przekazywanie przez referencję

function addOne(array &$arr): void // & znaczy: pracuj na oryginale, nie na kopii
{
$arr[] = 1;
}
$list = [];
addOne($list); // $list === [1]

Domyślnie argumenty są przekazywane przez wartość — funkcja dostaje kopię i nie może zmienić zmiennej wywołującego. Ampersand (&) w deklaracji to zmienia.

Często słyszy się, że obiekty w PHP przekazywane są „przez referencję”. To skrót myślowy i jest nieprawdziwy. Obiekt też idzie przez wartość — tylko tą wartością jest identyfikator obiektu, a nie sam obiekt. Kopiowany jest identyfikator, więc obie zmienne wskazują na ten sam obiekt:

function rename(User $u): void { $u->name = 'new'; } // zadziała — to wciąż ten sam obiekt
function replace(User $u): void { $u = new User('new'); } // NIE zadziała — podmienia tylko lokalną zmienną
$user = new User('old');
rename($user); // $user->name === 'new'
replace($user); // $user->name nadal 'new' — obiekt się nie zmienił

I właśnie dlatego to nie jest przekazywanie przez referencję: gdyby nim było, replace() podmieniłoby zmienną wywołującego. Żeby to osiągnąć, trzeba jawnie napisać &$u — dokładnie ta sama różnica, którą omawialiśmy przy referencjach w pierwszej części.

Konsekwencja praktyczna: nie potrzebujesz &, żeby zmienić stan obiektu wewnątrz funkcji. &$obj jest potrzebne wyłącznie wtedy, gdy chcesz podmienić samą zmienną wywołującego na inny obiekt.

Z & trzeba przy tym uważać, bo po stronie wywołania nic go nie zdradza. process($order) wygląda identycznie niezależnie od tego, czy funkcja tylko czyta obiekt, czy podmienia Ci zmienną pod nogami — żeby się dowiedzieć, trzeba zajrzeć do sygnatury. Dlatego w praktyce sięga się po to rzadko.


Zasięg zmiennych

To jest ta różnica, o której wspominałem na początku, i dla wielu osób największe zaskoczenie w całej serii.

$g = 10;
function f(): void
{
echo $g; // Warning: Undefined variable $g
}

Funkcja w PHP nie ma dostępu do zasięgu (ang. scope), w którym została zdefiniowana. Nie ma niejawnego closure jak w JavaScripcie, nie ma odczytu zmiennych zewnętrznych jak w Pythonie.

Żeby było jasne, co jest czym: zmienne utworzone poza jakąkolwiek funkcją trafiają do zasięgu globalnego — PHP nie ma czegoś takiego jak zasięg pliku czy modułu, więc $g = 10 na górze skryptu jest zmienną globalną. Wszystko, co powstaje wewnątrz funkcji, jest lokalne: parametry są zwykłymi zmiennymi lokalnymi i znikają razem z końcem wywołania, dokładnie tak samo jak zmienne przypisane w ciele funkcji. Te dwa światy się nie widzą — funkcja nie sięgnie po zmienną globalną, dopóki nie pobierze jej jawnie.

function f(): void
{
global $g; // działa, ale wiąże funkcję z globalnym stanem
echo $GLOBALS['g']; // to samo innym zapisem
}

Oba zapisy działają i oba są złym pomysłem: funkcja przestaje być testowalna i jej zależności znikają z sygnatury. Właściwa odpowiedź to przekazać wartość w argumencie.

PHP 8.1 zabronił nadpisywania całej tablicy $GLOBALS ($GLOBALS = […]). Zapis pojedynczych kluczy nadal działa.

Zmienne statyczne

function counter(): int
{
static $count = 0; // inicjalizowane RAZ, przeżywa między wywołaniami
return ++$count;
}
counter(); // 1
counter(); // 2

static wewnątrz funkcji to zmienna lokalna, która nie znika po zakończeniu wywołania funkcji. Przydaje się do memoizacji i leniwej inicjalizacji, ale ma kilka konsekwencji, o których warto wiedzieć:

  • static działa identycznie w zwykłej funkcji i w metodzie — zmienna jest przypisana do funkcji, a nie do pojedynczego wywołania ani do obiektu.
  • Dlatego zmienna statyczna w metodzie jest wspólna dla wszystkich instancji klasy: utworzenie dziesięciu obiektów nie daje dziesięciu liczników, tylko jeden wspólny. W zwykłej funkcji to oczywiste, w metodzie zaskakuje, bo intuicja podpowiada „po jednej na obiekt”.
  • Od PHP 8.1 metoda odziedziczona, której klasa potomna nie nadpisuje, dzieli tę samą zmienną statyczną z klasą bazową.

To znowu ukryty stan globalny, tyle że schowany w funkcji. W testach potrafi być bolesny.


Domknięcia

Zacznijmy od nazwy, bo mieszają się tu dwa pojęcia. Funkcja anonimowa to funkcja bez nazwy, zapisana jako zwykła wartość — można ją przypisać do zmiennej, przekazać dalej jako argument i wywołać później, tak samo jak robisz to z liczbą czy ze stringiem. Domknięcie (ang. closure) to natomiast nazwa mechanizmu: taka funkcja potrafi domknąć wokół siebie zmienne z miejsca, w którym powstała, i pamiętać je w chwili wywołania.

W PHP oba pojęcia zlewają się w jedno, bo każda funkcja anonimowa jest obiektem klasy Closure — niezależnie od tego, czy cokolwiek przechwytuje. Dlatego w praktyce, i w samej dokumentacji PHP, obu nazw używa się zamiennie.

Po co to komu: mnóstwo funkcji w PHP przyjmuje „kawałek logiki” jako argument — array_map(), array_filter(), usort(), obsługa zdarzeń, routing. Definiowanie nazwanej funkcji na górze pliku tylko po to, żeby przekazać ją dziesięć linii niżej, byłoby nieproporcjonalnie ciężkie.

A po co samo domykanie: zwykła funkcja nie widzi otoczenia — o tym była cała sekcja wyżej — więc bez tego mechanizmu nie dałoby się przekazać do callbacku niczego poza jego własnymi argumentami.

$min = 100;
$large = array_filter($orders, function (Order $o) use ($min): bool {
return $o->total >= $min; // $min pochodzi z zewnątrz — zwykła funkcja by go nie zobaczyła
});

PHP ma na to dwie składnie. Różnią się nie tylko zapisem, ale też momentem, w którym biorą wartość zmiennej — i to jest najczęstsza pułapka w całym tym tekście.

function () use ()

$mul = 3;
$f = function (int $x) use ($mul): int {
return $x * $mul;
};
$g = function (int $x) use (&$mul): int { // przechwycenie przez referencję
return $x * $mul;
};

Klauzula use pozwala jawnie zdecydować, co domknięcie zabierze ze sobą z otoczenia. Bez ampersanda robi kopię w momencie definicji, a nie wywołania — i to jest szczegół, który najczęściej gryzie:

$mul = 3;
$multiply = function (int $x) use ($mul): int { return $x * $mul; };
$mul = 10;
$multiply(2); // 6, a nie 20 — domknięcie zapamiętało trójkę w chwili, gdy powstawało

Funkcje strzałkowe

$mul = 3;
$f = fn(int $x): int => $x * $mul; // $mul przechwycone automatycznie, bez żadnego use

Funkcja strzałkowa (PHP 7.4) nie wymaga specjalnej składni do przechwytywania — sama sięga po te zmienne z otoczenia, których użyjesz w jej ciele. Płacisz za to jednym ograniczeniem: ciało może składać się z dokładnie jednego wyrażenia, które zawsze jest zwracane niejawnie. Nie ma tu miejsca na kilka instrukcji ani na własne return.

Przechwytywanie w fn jest zawsze przez wartość i nie da się tego zmienić — składnia fn() use (&$x) po prostu nie istnieje, PHP odrzuci ją błędem parsowania. Jeśli potrzebujesz wartości aktualnej w chwili wywołania, masz trzy wyjścia: pełne function () use (&$x), przekazanie wartości argumentem, albo przechwycenie obiektu — kopiowany jest wtedy identyfikator obiektu, więc jego stan widzisz na bieżąco:

$box = new Box(); // $box->v === 1
$scalar = 1;
$fromObject = fn() => $box->v;
$fromScalar = fn() => $scalar;
$box->v = 99;
$scalar = 99;
$fromObject(); // 99 — skopiowany został identyfikator, obiekt jest ten sam
$fromScalar(); // 1 — skopiowana została wartość
Diagram: closure z use przez referencję widzi aktualną wartość zmiennej, a use przez wartość i funkcja strzałkowa trzymają kopię z chwili definicji
Kopia powstaje w momencie definicji closure. Tylko use (&$x) sięga po aktualną wartość.

W kodzie wygląda to tak:

$x = 1;
$byValue = function () use ($x) { return $x; };
$byRef = function () use (&$x) { return $x; };
$arrow = fn() => $x;
$x = 99;
$byValue(); // 1 — kopia z chwili definicji
$byRef(); // 99 — referencja do żywej zmiennej
$arrow(); // 1 — funkcja strzałkowa też kopiuje
function () use () fn() =>
Przechwytywanie zmiennych jawne, przez use automatyczne, przez wartość
Ciało wiele instrukcji jedno wyrażenie
return jawny niejawny
Przechwycenie przez referencję use (&$x) niemożliwe

Praktycznie: funkcje strzałkowe do krótkich callbacków (array_map, usort, filtry), pełne closures tam, gdzie potrzebujesz kilku instrukcji albo referencji.


Callable

Zacznijmy od problemu, który callable rozwiązuje. W PHP nazwa funkcji nie jest wartością — inaczej niż w JavaScripcie, nie możesz po prostu napisać:

array_map(strlen, $words); // Error: Undefined constant "strlen"

PHP widzi tu nazwę stałej, a nie funkcji. Żeby przekazać funkcję tam, gdzie oczekiwana jest inna funkcja — na przykład do array_map() albo usort() — trzeba ją najpierw jakoś opakować. callable to zbiorcza nazwa na wszystkie zapisy, które PHP potrafi w takim miejscu wywołać, a uzbierało się ich sześć:

'strlen' // nazwa funkcji jako string
[$obj, 'method'] // metoda instancji
[Foo::class, 'staticMethod'] // metoda statyczna
'Foo::staticMethod' // to samo jako string
$obj // obiekt z metodą __invoke()
$closure // closure

Wszystkie sześć działa. array_map('strlen', $words) jest poprawnym kodem i zwróci dokładnie to, czego oczekujesz — nie musisz kombinować z funkcją strzałkową ani z use. Ta różnorodność to po prostu spadek po starym PHP i warto ją znać, żeby rozpoznawać te zapisy w cudzym kodzie.

Od PHP 8.1 jest jednak zapis lepszy od nich wszystkich — nie dlatego, że tamte nie działają, tylko dlatego, że są zwykłymi stringami i tablicami. Dla IDE 'strlen' to po prostu tekst w cudzysłowie: nie podpowie nazwy w trakcie pisania, nie pozwoli przeskoczyć do definicji, a przy zmianie nazwy funkcji nie ruszy tego stringa — literówkę zobaczysz dopiero, gdy ten kod się wykona.

Składnia f(...)

$fn = strlen(...);
$fn = $obj->method(...);
$fn = Foo::create(...);
array_map(strtoupper(...), $words); // to samo co array_map('strtoupper', $words), ale IDE widzi tu funkcję, nie tekst

Zapis bierze istniejącą, nazwaną funkcję i pakuje ją w obiekt klasy Closure. Najważniejsze jest to, że ujednolica sposób przekazywania funkcji jako wartości: nieważne, czy chodzi o funkcję globalną, metodę obiektu czy metodę statyczną — zapis jest zawsze ten sam, zamiast sześciu różnych. Do tego jest sprawdzany przez kompilator, rozumiany przez IDE i przenoszony przez refaktoryzację. W nowym kodzie używaj tylko tej formy.

Uwaga, żeby nie pomylić pojęć: strlen(...) daje obiekt klasy Closure, ale nie mamy tu do czynienia z mechanizmem domknięcia. Nazwana funkcja nie ma żadnego otoczenia do przechwycenia, więc nic się tu nie domyka i strlen nie zaczyna nagle widzieć Twoich zmiennych. Closure jest w tym miejscu po prostu nazwą klasy, w którą PHP pakuje „coś wywoływalnego”. Faktycznie przechwytują dopiero function () use (…) i fn().

Przed PHP 8.1 odpowiednikiem było Closure::fromCallable('strlen').

Zmienne funkcyjne

Zmienna funkcyjna to wywołanie, w którym w miejscu nazwy funkcji stawiasz zmienną — piszesz $nazwa('hi') tam, gdzie normalnie byłoby strtoupper('hi'). PHP bierze wtedy zawartość tej zmiennej i traktuje ją jako nazwę funkcji do wywołania. Nazwa rozwiązywana jest dopiero w momencie wywołania — nic nie sprawdza wcześniej, czy taka funkcja w ogóle istnieje. To mechanizm starszy niż f(...) i stąd jego ograniczenia opisane niżej.

$fn = 'strtoupper';
$fn('hi'); // 'HI' — nazwa rozwiązywana w momencie wywołania
is_callable($fn); // true
'strtoupper'('hi'); // też zadziała — PHP 7+ pozwala wywołać string wprost, ale po co, skoro masz strtoupper('hi')

Nie zadziała to dla konstrukcji językowych, bo one nie są funkcjami: echo, print, isset, empty, unset, include, require, list, array. Jeśli potrzebujesz którejś jako callbacku, opakuj ją:

$isSet = fn($x) => isset($x);

callable czy Closure w sygnaturze

Skoro oba da się wpisać jako typ, warto wiedzieć, czym się różnią. callable przyjmuje każdą z sześciu form powyżej. Closure — wyłącznie obiekty tej klasy, czyli funkcje anonimowe, strzałkowe i zapis f(...):

function run(callable $c): string { return $c('hi'); }
run('strtoupper'); // OK — string z nazwą funkcji przechodzi
run(strtoupper(...)); // OK — Closure też jest callable
function runStrict(Closure $c): string { return $c('hi'); }
runStrict(strtoupper(...)); // OK
runStrict('strtoupper'); // TypeError: must be of type Closure, string given

Closure jest węższy i zwykle właściwszy: masz gwarancję, że dostałeś coś wywoływalnego tu i teraz, a nie string z nazwą, która może wskazywać na nieistniejącą funkcję — callable sprawdzi to dopiero w momencie wywołania.

Jest też twarde ograniczenie: callable nie może być typem właściwości.

class Button
{
public Closure $onClick; // OK
public callable $onHover; // Fatal error: Property Button::$onHover cannot have type callable
}

Przy okazji pułapka, na którą trafia każdy, kto pierwszy raz trzyma domknięcie we właściwości:

$b = new Button();
$b->onClick = fn() => 'klik';
$b->onClick(); // Error: Call to undefined method Button::onClick()
($b->onClick)(); // 'klik' — nawiasy mówią PHP, że chodzi o właściwość, a nie o metodę

Wiązanie $this

Domknięcie utworzone w ciele metody automatycznie widzi $this — nie trzeba go nigdzie wymieniać w use:

class Cart
{
private float $rate = 1.23;
public function grossPrices(array $items): array
{
return array_map(fn(float $net): float => $net * $this->rate, $items);
// ↑ $this dostępne bez żadnych zabiegów
}
}

Osobna sprawa to domknięcie utworzone poza klasą. Takie nie ma żadnego $this — ale da się mu je dopiąć za pomocą Closure::bind():

class Counter
{
private int $n = 5;
}
$counter = new Counter();
$read = function () { return $this->n; };
$bound = Closure::bind($read, $counter, Counter::class);
echo $bound(); // 5 — ma dostęp do prywatnej właściwości

Drugi argument to obiekt, który ma zostać $this wewnątrz domknięcia. Trzeci to zasięg klasowy — czyli odpowiedź na pytanie „z wnętrza której klasy ten kod niby się wykonuje”. To on, a nie drugi argument, decyduje o dostępie do składowych prywatnych i chronionych. Do wyboru masz:

  • nazwę klasy (Counter::class) — domknięcie działa tak, jakby stało w ciele tej klasy, więc widzi jej private i protected;
  • obiekt (new Counter()) — PHP weźmie z niego nazwę klasy, efekt jest ten sam;
  • null — brak zasięgu, domknięcie zobaczy wyłącznie składowe publiczne;
  • pominięcie argumentu — wartością domyślną jest 'static', co znaczy „zostaw zasięg, który domknięcie już ma”. Dla domknięcia utworzonego poza klasą żadnego nie ma, więc efekt jest taki sam jak przy null: próba odczytu $this->n z przykładu wyżej skończy się Error: Cannot access private property.

$read jest obiektem klasy Closure, więc ma też własne metody — i stąd zapis z ->:

$read->call($counter); // wiązanie i wywołanie w jednym; zasięgiem staje się klasa przekazanego obiektu

call() to zwyczajna metoda instancji, tak samo jak bindTo(); Closure::bind() jest jej statycznym odpowiednikiem. Różnica jest w trwałości: bind() i bindTo() zwracają nowe domknięcie do dalszego użytku, a call() wykonuje je od razu, jednorazowo.

Jest jeden wyjątek — domknięcie statyczne, czyli zadeklarowane ze słowem static przed function albo fn. Takie z definicji nie ma $this i nie da się mu go dopiąć:

$fn = static fn(): int => 1;
Closure::bind($fn, $counter, Counter::class);
// Warning: Cannot bind an instance to a static closure — zwraca null (w PHP 9 będzie to błąd)

Wiązanie $this to narzędzie do testów, adapterów i hydratorów, a nie do codziennego kodu — omija enkapsulację, więc jeśli sięgasz po nie w normalnej logice biznesowej, prawdopodobnie klasa ma zły interfejs.

Samo static przed domknięciem przydaje się natomiast poza wiązaniem. Domknięcie utworzone w metodzie trzyma $this nawet wtedy, gdy w ogóle go nie używa — a dopóki trzyma, obiekt nie może zostać zwolniony:

class Report
{
public function __destruct() { echo "zwolniony\n"; }
public function formatter(): Closure
{
return fn(float $v): string => number_format($v, 2); // trzyma $this, choć go nie używa
}
public function staticFormatter(): Closure
{
return static fn(float $v): string => number_format($v, 2); // nie trzyma niczego
}
}
$fn = (new Report())->formatter();
// cisza — obiekt Report żyje dalej, bo domknięcie trzyma $this
unset($fn);
// "zwolniony" — dopiero teraz, razem z domknięciem
$fn = (new Report())->staticFormatter();
// "zwolniony" — od razu, domknięcie nie miało za co obiektu trzymać

Dlatego przy callbackach przekazywanych z metody gdzieś dalej — do array_map(), do kolejki zadań, do kontenera zdarzeń — warto dopisać static, jeśli $this w środku nie jest potrzebne. Inaczej czas życia obiektu zaczyna zależeć od tego, jak długo ktoś przechowuje Twój callback.


Co dalej

Umiesz już opakować logikę w funkcję i przekazać ją dalej jako wartość. Następna część jest krótsza i bardzo konkretna: daty i czas.

Zobaczymy, dlaczego DateTime i DateTimeImmutable to dwie różne klasy i którą wybierać domyślnie, jak działa DateInterval i DatePeriod, co naprawdę robi strtotime() i dlaczego strefy czasowe są w PHP osobnym tematem, który trzeba obsłużyć świadomie.

Komentarze