Podstawy PHP w pigułce: Funkcje i closures
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|nullZnak 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 stringfunction render(Stringable&Countable $x): void { } // przecięcie (PHP 8.1) — Stringable I Countable narazUnia 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 narazrender('koszyk'); // TypeError — string nie jest ani jednym, ani drugimKlasy 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ę wykonujefunction fail(): never { throw new … ; } // never (PHP 8.1) — funkcja NIGDY się nie kończyRóż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 nullemSkoro 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 ModelGdyby 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 kompilacjifunction bad(int $x = random_int(1, 10)) { } // błąd — wywołanie funkcjiOd 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 funkcjiNazwy 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ślnychArgumenty 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); // 6Trzy 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 obiektfunction 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.&$objjest 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(); // 1counter(); // 2static 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ć:
staticdział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łoFunkcje strzałkowe
$mul = 3;
$f = fn(int $x): int => $x * $mul; // $mul przechwycone automatycznie, bez żadnego useFunkcja 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
fnjest zawsze przez wartość i nie da się tego zmienić — składniafn() 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łnefunction () 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ść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ż kopiujefunction () 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 // closureWszystkie 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 tekstZapis 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 klasyClosure, 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 istrlennie zaczyna nagle widzieć Twoich zmiennych.Closurejest w tym miejscu po prostu nazwą klasy, w którą PHP pakuje „coś wywoływalnego”. Faktycznie przechwytują dopierofunction () use (…)ifn().
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 przechodzirun(strtoupper(...)); // OK — Closure też jest callable
function runStrict(Closure $c): string { return $c('hi'); }
runStrict(strtoupper(...)); // OKrunStrict('strtoupper'); // TypeError: must be of type Closure, string givenClosure 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ściDrugi 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 jejprivateiprotected; - 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 przynull: próba odczytu$this->nz 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 obiektucall() 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 $thisunset($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.