Middle: питання на співбесіді з теми «Функції й замикання»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Анонімні й стрілкові функції в PHP - об'єкти класу Closure. Цей клас має методи для керування контекстом виконання.
Closure::fromCallable() - створює замикання з будь-якого callable: імені функції, [$object, 'method'], [Class::class, 'staticMethod']. Сьогодні замість нього пишуть first-class callable syntax:
$fn = Closure::fromCallable([$this, 'normalize']);
$fn = $this->normalize(...); // те саме, з PHP 8.1
bind() / bindTo() - змінюють $this і область видимості класу (scope) замикання:
final class Counter
{
private int $count = 5;
}
$peek = function (): int {
return $this->count; // приватна властивість
};
$bound = Closure::bind($peek, new Counter(), Counter::class);
echo $bound(); // 5
Другий аргумент - новий $this, третій - клас, від імені якого замикання має доступ до приватних і захищених членів.
call() - тимчасово прив'язати й одразу викликати: $peek->call(new Counter()).
Де це використовують:
- Макроси в Laravel (
Collection::macro('toUpper', function () { return $this->map(...); })) - замикання прив'язується до екземпляра колекції, тож$thisусередині - колекція. - Тести й налагодження - доступ до приватного стану без Reflection.
- DSL і конфігурація - виконати колбек «всередині» об'єкта-будівника.
Застереження: доступ до приватних членів через bind обходить інкапсуляцію. У робочому коді це майже завжди ознака, що потрібен публічний метод. Статичне замикання (static function) прив'язати до об'єкта не можна взагалі.
Замикання, створене всередині методу класу, автоматично захоплює $this - навіть якщо не використовує його. Ключове слово static вимикає це:
final class ReportBuilder
{
public function totals(array $rows): array
{
return array_map(static fn (array $row): int => $row['qty'] * $row['price'], $rows);
}
}
Навіщо:
- Пам'ять і час життя об'єкта. Звичайне замикання тримає посилання на
$this. Якщо його зберегли надовго (у кеші, в реєстрі обробників подій, у властивості іншого об'єкта), то й весь об'єкт з усіма залежностями не звільниться. У довгоживучих процесах (Octane, воркери) це шлях до витоків пам'яті. - Ясність намірів.
staticпоказує читачу й аналізатору: колбек не залежить від стану об'єкта, це чиста функція даних. - Захист від випадкового доступу: використання
$thisу статичному замиканні - помилка, а не тиха залежність. - Незначна економія: менше роботи на прив'язку контексту.
Обмеження: статичне замикання не можна прив'язати до об'єкта через bind()/bindTo(). Тому в місцях, де фреймворк навмисно прив'язує замикання (макроси Laravel, деякі колбеки конфігурації), static ламає поведінку.
Практика: багато команд вмикають правило PHP-CS-Fixer static_lambda, яке автоматично додає static до замикань, що не використовують $this. Rector має аналогічне правило.
Те саме для стрілкових функцій: static fn - стрілкова функція без прив'язки $this, але з автоматичним захопленням інших зовнішніх змінних.
Рекурсія - функція викликає сама себе. Природна для деревовидних даних: каталоги, коментарі з відповідями, категорії, розбір вкладеного JSON.
function totalSize(array $node): int
{
$size = $node['size'];
foreach ($node['children'] as $child) {
$size += totalSize($child);
}
return $size;
}
Обмеження в PHP:
- Кожен виклик займає пам'ять у стеку викликів. Глибина в десятки тисяч рівнів може вичерпати стек.
- Немає оптимізації хвостової рекурсії: навіть
return f($n - 1)наприкінці функції займає новий кадр стеку. - Наслідки переповнення залежать від версії й платформи. PHP 8.3 додав налаштування
zend.max_allowed_stack_size, з яким рушій кидаєError«Maximum call stack size reached» замість падіння. Але на частині конфігурацій процес усе одно аварійно завершується (segfault) без жодного стек-трейсу. З Xdebug діє власний лімітxdebug.max_nesting_level.
Як обійти глибоку рекурсію - ітерацією з явним стеком:
function totalSize(array $root): int
{
$size = 0;
$stack = [$root];
while ($stack !== []) {
$node = array_pop($stack);
$size += $node['size'];
foreach ($node['children'] as $child) {
$stack[] = $child;
}
}
return $size;
}
Стек тепер - звичайний масив у купі, обмежений лише memory_limit.
Інші варіанти:
- Генератори з
yield from- обхід дерева без накопичення результатів у пам'яті. - Рекурсія в базі даних -
WITH RECURSIVEдля ієрархій, що зберігаються в таблиці, замість рекурсивних запитів з PHP (і N+1).
Захист від нескінченної рекурсії: умова зупинки на першому рядку і, для даних ззовні (зациклені посилання в графі), - облік уже відвіданих вузлів чи обмеження глибини.
Мемоізація - кешування результату функції для вже бачених аргументів: повторний виклик з тими самими аргументами повертає збережене значення без повторних обчислень.
Статична змінна у функції:
function countryName(string $code): string
{
static $cache = [];
return $cache[$code] ??= Country::where('code', $code)->value('name');
}
Властивість об'єкта - кеш живе разом з екземпляром сервісу:
final class ExchangeRates
{
private array $rates = [];
public function rate(string $currency): float
{
return $this->rates[$currency] ??= $this->fetchFromApi($currency);
}
}
WeakMap - коли аргумент - об'єкт, і кеш не має утримувати його в пам'яті:
private WeakMap $totals;
public function total(Order $order): int
{
return $this->totals[$order] ??= $this->calculate($order);
}
У Laravel є хелпер once(): результат замикання кешується на час запиту в межах конкретного об'єкта й місця виклику.
public function permissions(): array
{
return once(fn () => $this->loadPermissionsFromDatabase());
}
Коли мемоізація доречна: функція чиста (той самий вхід - той самий результат), виклики з однаковими аргументами повторюються, а обчислення дороге.
Пастки:
- Довгоживучі процеси. Статична змінна в PHP-FPM живе один запит, а в Octane чи воркері черги - доки живе процес. Кеш накопичується і може віддавати застарілі дані одного користувача іншому.
- Нечисті функції: мемоізувати «поточного користувача» чи «час зараз» - отримати застарілі значення.
- Ключ кешу для кількох аргументів - серіалізувати їх однозначно (
serialize,json_encode), пам'ятаючи про об'єкти. - Мемоізація ≠ кеш застосунку. Вона живе в пам'яті процесу; для спільного між запитами кешу - Redis і
Cache::remember.