Александр Некрасов · Автономное руководство

PHP: полный мануал с примерами, функциями и ООП

Разделы этой страницы
  1. Замыкания, callback и стрелочные функции
  2. Сохранить правило расчёта и вызвать его позже
  3. Callback: передать правило, чтобы его вызвала другая функция
  4. Use: настройка берётся при создании, цена — при вызове
  5. Замыкание — значение, которое хранит код и нужное окружение
  6. $this, static-замыкание и срок жизни объекта
  7. Стрелочная функция: одно выражение и автоматический захват
  8. First-class callable: сохранить разрешённый вызов на потом
  9. PHP 8.5: правило по умолчанию прямо в объявлении
  10. Совместимость со старым API и путь к современному контракту
  11. Функция может быть значением
  12. Значение use и ссылка use(&...)
  13. Чистый callback легче менять и проверять
  14. Где заканчивается удобство
  15. Closure: готовое действие, которое можно хранить и передавать
  16. Четыре детали замыкания: тело, захват, $this и область доступа
  17. Как выбрать метод Closure и не перепутать подготовку с выполнением
  18. Хранить функцию в свойстве — ещё не добавить метод
  19. Копия Closure и срок жизни связанных объектов
  20. Исследовать функцию, не выполняя её и не публикуя окружение
  21. Область доступа не является песочницей или авторизацией
  22. Функции и методы этой темы
  23. Код и руководство с собой
PHP 8.5.11149 глав4458 карточек справочника
Дерево руководства

Глава 16 · С нуля → практика

Замыкания, callback и стрелочные функции

Передаем правило обработки данных и понимаем, когда захватывается внешнее значение.

Сохранить правило расчёта и вызвать его позже

Для двух заказов нужно одно правило: к цене прибавить доставку 50. Обычную функцию уже умеем объявлять и вызывать. Теперь сохраним само правило в переменной, чтобы позже передать его другой части программы. Начнём без захвата внешних переменных, сокращений fn и фабрик.

ЗаписьЧто делает
function ($price) { ... }Создаёт функцию без собственного имени. price — местный параметр будущего вызова.
$withDelivery = ...;Сохраняет функцию как значение; завершающая ; относится к присваиванию. Тело пока не выполняется.
$withDelivery(150)Скобки вызывают сохранённую функцию; аргумент 150 поступает в price.
return $price + 50;Возвращает число в то выражение, которое вызвало функцию.
Подготовить правило, затем рассчитать два заказаСамостоятельный пример
<?php
$withDelivery = function ($price) {
    return $price + 50;
};
echo "Правило подготовлено\n";
$firstTotal = $withDelivery(150);
$secondTotal = $withDelivery(200);
echo 'Первый заказ: ', $firstTotal, ' руб.', "\n";
echo 'Второй заказ: ', $secondTotal, ' руб.', "\n";
Ожидаемый результат
Правило подготовлено
Первый заказ: 200 руб.
Второй заказ: 250 руб.

Создание функции не вычисляет сумму. Первый вызов получает 150, создаёт свой местный price=150 и возвращает 200 в firstTotal. Второй независимо получает 200 и возвращает 250 в secondTotal. Только внешние echo показывают обе суммы. Значение функции в PHP представлено объектом Closure; для этого первого вызова не требуется создавать собственный класс.

Первый вызов по шагам
  1. $withDelivery(150) ожидает результат.
  2. Внутри price=150; выражение price+50 равно 200.
  3. Return завершает этот вызов и возвращает 200.
  4. Присваивание записывает 200 в firstTotal; echo использует готовое число.

Callback: передать правило, чтобы его вызвала другая функция

Callback — роль функции: её передали как значение, а получатель вызовет её в нужный момент. Пока делегируем один расчёт. В аргументе $rule нет круглых скобок: передаётся функция. Внутри получателя $calculate($price) уже есть скобки и начинается вызов.

Проследить два возврата в одном расчётеСамостоятельный пример
<?php
function prepareAmount($price, $calculate) {
    $amount = $calculate($price);
    return 'К оплате: ' . $amount . ' руб.';
}
$rule = function ($price) {
    return $price + 50;
};
$message = prepareAmount(150, $rule);
echo $message, "\n";
Ожидаемый результат
К оплате: 200 руб.

Внешний вызов передаёт price=150 и саму функцию rule в параметр calculate. Вызов calculate(150) входит в анонимную функцию; её return 200 возвращается в местное amount функции prepareAmount. Затем второй return возвращает строку «К оплате: 200 руб.» в message. Echo читает эту строку. Если вместо rule передать rule(150), получатель получил бы число 200 и не смог бы вызвать его.

Встроенные функции задают свой договор callback: какие параметры передаются, сколько раз возможен вызов и куда попадает return. У array_map возврат становится новым элементом; у array_filter — решением оставить элемент. Эти договоры нельзя переносить друг на друга: примеры map и filter.

Use: настройка берётся при создании, цена — при вызове

Доставка теперь хранится отдельно: 50 рублей. Сохраним её в правиле через use ($delivery). Параметр price по-прежнему принимает новую цену при каждом вызове. Так одно правило хранит свою настройку и не читает произвольное внешнее имя при каждом запуске.

Старое и новое правило помнят разную доставкуСамостоятельный пример
<?php
$delivery = 50;
$oldRule = function ($price) use ($delivery) {
    return $price + $delivery;
};
$delivery = 100;
$newRule = function ($price) use ($delivery) {
    return $price + $delivery;
};
echo 'Прежнее правило для 150: ', $oldRule(150), ' руб.', "\n";
echo 'Новое правило для 150: ', $newRule(150), ' руб.', "\n";
Ожидаемый результат
Прежнее правило для 150: 200 руб.
Новое правило для 150: 250 руб.

OldRule создаётся при delivery=50 и сохраняет именно это число. Внешнее присваивание 100 не меняет его окружение. NewRule создаётся позже и сохраняет 100. Оба вызова получают один аргумент 150, но складывают с разными сохранёнными настройками. Use не создаёт глобальную переменную. Проще всего мыслить это как правило плюс его снимок числовых настроек; для объектов такая модель требует уточнения ниже.

Замыкание — значение, которое хранит код и нужное окружение

Фабрика форматтера использует уже разобранные два этапа: создать правило с prefix, затем вызвать с sku. Trim убирает только крайние пробелы; оператор . соединяет prefix и очищенный артикул. Вторая часть ниже отдельно демонстрирует намеренный внешний счётчик через use(&$processed).

Нужен форматтер для конкретного магазина: один раз запоминаем префикс SHOP-, затем применяем его к множеству SKU. function (...) use (...) { ... } создаёт объект Closure. Присваивание заканчивается точкой с запятой, а тело выполнится только при вызове.

Параметры передают новые значения при каждом вызове. use ($prefix) фиксирует значение в момент создания. use (&$processed) разделяет переменную, изменения которой видны обеим сторонам. Это окружение определения, а не глобальная область и не место будущего вызова.

Создать один раз, вызывать несколько раз
  1. makeSkuFormatter получает SHOP-
  2. Closure сохраняет prefix и код
  3. Внешний код передаёт TEA
  4. Возвращается SHOP-TEA
Фабрика обработчика помнит настройки места созданияСамостоятельный пример
<?php
function makeSkuFormatter($prefix) {
    return function ($sku) use ($prefix,): string {
        return $prefix . trim($sku);
    };
}
$format = makeSkuFormatter('SHOP-');
$prefix = 'OTHER-';
echo $format(' TEA '), "\n";
echo $format instanceof Closure ? "Это Closure\n" : "Другой тип\n";

$processed = 0;
$remember = function () use (&$processed) {
    ++$processed;
};
$remember();
$remember();
echo 'Обработано: ', $processed, "\n";
Ожидаемый результат
SHOP-TEA
Это Closure
Обработано: 2

prefix захвачен при создании, поэтому поздняя одноимённая переменная снаружи не заменяет SHOP-. Возвращённое замыкание живёт и после завершения makeSkuFormatter. В счётчике use(&$processed) намеренно разделяет изменяемую переменную. Тип возврата : string у анонимной функции записывается после use(...).

В use нельзя включать superglobals, $this или имя, уже занятое параметром; ограничения действуют с PHP 7.1. Superglobals доступны по собственным правилам, а $this связывается из объектного контекста. Завершающая запятая в use допустима с PHP 8.0.

Захват объекта по значению сохраняет идентификатор того же объекта: его свойства всё ещё могут измениться. Для неизменяемой конфигурации выбирайте соответствующий договор, а не рассчитывайте, что use автоматически глубоко скопирует всё.

$this, static-замыкание и срок жизни объекта

Callback иногда должен читать настройки объекта, а иногда выполняет независимое преобразование строки. Обычное замыкание, созданное в методе экземпляра, получает связь с этим объектом; static-замыкание создают, когда связь не нужна.

Замыкание метода помнит объект; static явно обходится без негоСамостоятельный пример
<?php
class ExportLabel {
    private string $prefix = 'EXPORT-';
    public function formatter(): Closure {
        return function (string $sku): string {
            return $this->prefix . $sku;
        };
    }
    public function independent(): Closure {
        return static function (string $sku): string {
            return (isset($this) ? 'object:' : 'no-this:') . $sku;
        };
    }
}
$labels = new ExportLabel();
$bound = $labels->formatter();
$independent = $labels->independent();
echo $bound('TEA'), "\n";
echo $independent('TEA'), "\n";
Ожидаемый результат
EXPORT-TEA
no-this:TEA

В обычном замыкании метода доступен текущий $this: оно хранит связь с объектом. static-замыкание её не получает. Это полезно для независимого callback, который не должен держать объект ради вычисления. static здесь не создаёт глобальную функцию или общий кэш.

Попытка использовать $this как объект внутри static-замыкания приводит к ошибке; привязать к нему экземпляр через bindTo тоже нельзя. Область класса и связанный экземпляр — разные вещи: static не обязательно стирает область доступа класса.

Если долго хранить callback, привязанный к объекту, он может продлить жизнь этого объекта и данных, на которые тот ссылается. В очереди или долгом worker это важнее, чем в коротком запросе. Не объявляйте static просто ради непроверенного обещания ускорения.

Стрелочная функция: одно выражение и автоматический захват

Форма fn($amount) => $amount + $fee коротко описывает один расчёт. Результат выражения возвращается автоматически. Используемые внешние переменные захватываются по значению; писать use не нужно. Синтаксис доступен с PHP 7.4.

Если алгоритму нужны несколько действий, try/catch, изменяемый накопитель или подробные ветви, обычное замыкание читается лучше. В одном выражении не стоит прятать целый процесс импорта.

Укоротить только одно уже понятное выражениеСамостоятельный пример
<?php
$fee = 50;
$withFee = fn($amount) => $amount + $fee;
$fee = 99;
$payable = $withFee(100);
echo 'Сумма с сохранённой доставкой: ', $payable, ' руб.', "\n";
Ожидаемый результат
Сумма с сохранённой доставкой: 150 руб.

Fn начинает стрелочную функцию, скобки перечисляют параметр amount, а => вводит единственное возвращаемое выражение. Return не пишется. Использованная внешняя fee автоматически сохранена со значением 50, когда создавалась функция. Вызов передаёт 100, получает 150 и сохраняет его в payable. Этот => принадлежит синтаксису fn; это не пара ключ/значение массива.

Разделить создание форматтера и форматирование артикулаСамостоятельный пример
<?php
$factory = fn($prefix) => fn($sku) => $prefix . $sku;
$shopFormat = $factory('SHOP-');
$label = $shopFormat('TEA');
echo 'Этикетка: ', $label, "\n";
Ожидаемый результат
Этикетка: SHOP-TEA

Первая стрелка возвращает значение выражения fn($sku)..., то есть новую функцию, а не строку. Вызов factory(SHOP-) сохраняет prefix внутри этой новой функции и присваивает её shopFormat. Только следующий вызов shopFormat(TEA) получает sku и возвращает строку SHOP-TEA в label. Это короткая запись уже изученной фабрики; при сложной логике её раскрывают через function/use/return.

Не спутать снимок внешнего числа с параметром по ссылкеСамостоятельный пример
<?php
$count = 0;
$localIncrease = fn() => ++$count;
$inside = $localIncrease();
echo 'Результат выражения: ', $inside, '; внешний счётчик: ', $count, "\n";
$increase = fn(&$value) => ++$value;
$increase($count);
echo 'После явного изменения параметра: ', $count, "\n";
Ожидаемый результат
Результат выражения: 1; внешний счётчик: 0
После явного изменения параметра: 1

Первая стрелка захватила count по значению; её ++ вернул 1, но внешний count остался 0. Во второй & относится к объявленному параметру value. Аргумент count связывается с ним на время вызова; ++ действительно меняет внешнюю переменную на 1. Автоматический захват и явный параметр по ссылке — разные механизмы. Для обычного подсчёта в цикле явное count++ снаружи понятнее.

Стрелочные функции допускают типы, defaults, variadic, передачу/возврат ссылки и static, как другие подходящие сигнатуры. Например static fn(int $n = 1): int => $n * 2; редкий fn&(...) => ... требует тех же правил возврата ссылки.

Захват по значению не гарантирует отсутствия эффектов: выражение может вызвать запись, изменить объект или получить явный параметр по ссылке. Выбирайте fn за читаемость небольшого преобразования, а не как автоматическую гарантию «чистой функции».

First-class callable: сохранить разрешённый вызов на потом

Нужна уже написанная функция как обработчик для array_map, маршрута или очереди событий. В PHP 8.1+ $handler = formatSku(...) создаёт Closure с этим вызовом; это не запуск и не распаковка массива. Результат можно передать, вернуть или сохранить как значение.

Сохранить существующий форматтер и вызвать в нужный моментСамостоятельный пример
<?php
function formatSku(string $sku): string {
    echo 'Форматируем ', $sku, "\n";
    return '[' . $sku . ']';
}
$format = formatSku(...);
echo "Форматтер сохранён\n";
$label = $format('TEA');
echo 'Этикетка: ', $label, "\n";
$equivalent = Closure::fromCallable('formatSku');
$secondLabel = $equivalent('MILK');
echo 'Вторая этикетка: ', $secondLabel, "\n";
Ожидаемый результат
Форматтер сохранён
Форматируем TEA
Этикетка: [TEA]
Форматируем MILK
Вторая этикетка: [MILK]

В записи formatSku(...) три точки — буквальный синтаксис PHP 8.1+, создающий Closure, без вызова тела. Поэтому первой видна строка «Форматтер сохранён». Вызов format(TEA) выводит диагностическую строку из тела и возвращает [TEA] в label. Closure::fromCallable — метод встроенного класса, создающий соответствующий обработчик по имени. Он вызван через :: без собственного объекта. Второй обработчик получает MILK и возвращает [MILK] в secondLabel. Диагностический echo показывает момент выполнения тела; в прикладном форматтере его обычно убирают.

Передать готовую функцию другой операцииСамостоятельный пример
<?php
$length = strlen(...);
$names = ['TEA', 'COFFEE'];
$lengths = array_map($length, $names);
foreach ($lengths as $position => $bytes) {
    echo $names[$position], ': ', $bytes, ' байт', "\n";
}
Ожидаемый результат
TEA: 3 байт
COFFEE: 6 байт

Length хранит вызываемую strlen. Array_map сам вызывает её с TEA, затем COFFEE и собирает возвращённые 3 и 6 в lengths. Это длина в байтах; здесь ASCII, поэтому совпадает с количеством букв. Следующий foreach соединяет каждое число с исходным названием той же позиции. Передача length без скобок даёт правило; вызов length(TEA) дал бы лишь одно число.

Разрешить конкретный private-обработчик через ClosureСамостоятельный пример
<?php
class ImportFormatter {
    private function format(string $sku): string {
        return 'IMPORT:' . $sku;
    }
    public function callback(): Closure {
        return $this->format(...);
    }
    public function oldStyle(): array {
        return [$this, 'format'];
    }
}
$formatter = new ImportFormatter();
$callback = $formatter->callback();
echo $callback('TEA'), "\n";
$old = $formatter->oldStyle();
try {
    $old('TEA');
} catch (Error $error) {
    echo "Массив не перенёс право доступа к private\n";
}
Ожидаемый результат
IMPORT:TEA
Массив не перенёс право доступа к private

Метод callback намеренно выдаёт возможность конкретного вызова, созданную в разрешённой области класса. Массив лишь хранит объект и имя; проверка доступа при внешнем вызове не разрешает private. Это не способ произвольно обходить область видимости чужого объекта.

ЗаписьЧто означает
strlen(...)Closure для обычной функции
$service->method(...)Closure для метода конкретного объекта
Service::method(...)Closure для доступного статического метода
$callable(...)Closure из вызываемого значения, включая объект с __invoke
Closure::fromCallable($callable)Семантически соответствующая явная фабрика

Видимость проверяется в месте создания. После этого Closure несёт разрешённый вызов, в отличие от массива с именем private-метода. Решение выдать такой обработчик наружу принимает сам класс.

Этот синтаксис не работает с конструктором new ClassName(...) и с nullsafe-цепочкой $object?->method(...). Для фабрики создайте обычное замыкание с new; nullable-объект проверьте до создания обработчика. Здесь это текстовые ограничения, не исполняемые ошибочные файлы.

PHP 8.5: правило по умолчанию прямо в объявлении

У функции печати этикетки есть обычный формат, но вызывающий может передать другой. Начиная с PHP 8.5 замыкание допускается в константном выражении: в default параметра/свойства, константе и аргументе атрибута. Для анонимной функции в таком месте обязательна форма static function без use; fn не допускается из-за неявного захвата. Static здесь означает отсутствие связанного $this. Это правило для места создания функции; её тело выполняется позже обычным вызовом.

Использовать стандартный формат и заменить его своимСамостоятельный пример
<?php
// PHP 8.5+.
function prepareLabel(string $sku, Closure $format = static function (string $value): string {
    return '[' . $value . ']';
}): string {
    return $format($sku);
}
$normal = prepareLabel('TEA');
$custom = prepareLabel('TEA', function ($value) { return 'SHOP-' . $value; });
echo 'Стандарт: ', $normal, "\n";
echo 'Своё правило: ', $custom, "\n";
Ожидаемый результат
Стандарт: [TEA]
Своё правило: SHOP-TEA

В первом вызове второй аргумент опущен, поэтому параметр format получает стандартную Closure. Тело prepareLabel вызывает её с TEA; внутренний return даёт [TEA], внешний возвращает ту же строку в normal. Во втором вызове явное правило заменяет default и возвращает SHOP-TEA в custom. Тип Closure означает именно объект замыкания, не любую строку с именем функции. Для PHP 8.2–8.4 стандартное правило можно создать в теле после явного nullable-параметра.

В PHP 8.5 здесь допускается также first-class callable: имя обычной функции или доступного static-метода задаётся непосредственно в синтаксисе. Динамическое имя из переменной/выражения, метод экземпляра и метод, существующий лишь через __callStatic, для такого константного выражения не подходят. Проверка доступа остаётся в области создания; класс при необходимости автозагружается. Для неквалифицированного имени функции в namespace действует поиск в нём с обычным переходом к глобальной функции.

Сохранить существующее правило константойСамостоятельный пример
<?php
// PHP 8.5+.
function bracketSku(string $sku): string {
    return '[' . $sku . ']';
}
const DEFAULT_LABEL = bracketSku(...);
$formatter = DEFAULT_LABEL;
$label = $formatter('COFFEE');
echo 'Этикетка: ', $label, "\n";
Ожидаемый результат
Этикетка: [COFFEE]

Const объявляет именованное неизменяемое значение DEFAULT_LABEL. BracketSku(...) создаёт вызываемое значение, а не строку этикетки. Обычное присваивание берёт его в formatter; только formatter(COFFEE) выполняет тело и возвращает [COFFEE]. Нельзя заменить bracketSku на произвольное выражение имени внутри этого константного синтаксиса. В версиях до 8.5 то же правило храните обычной переменной после объявления функции.

Точные версионные ограничения: реализованный RFC замыканий в константных выражениях и реализованный RFC first-class callable. Они дополняют изменения, отмеченные в PHP Manual.

Совместимость со старым API и путь к современному контракту

Читать старый код с func_num_args, func_get_arg и func_get_argsСамостоятельный пример
<?php
function describeCall($sku = 'TEA', $quantity = 1) {
    $count = func_num_args();
    $first = $count > 0 ? func_get_arg(0) : '(нет)';
    return $count . '|' . $first . '|'
        . json_encode(func_get_args(), JSON_THROW_ON_ERROR);
}
echo describeCall(), "\n";
echo describeCall('COFFEE', 2, 'legacy-extra'), "\n";
echo describeCall(quantity: 3), "\n";
Ожидаемый результат
0|(нет)|[]
3|COFFEE|["COFFEE",2,"legacy-extra"]
2|TEA|["TEA",3]

В пользовательскую функцию могут попасть дополнительные позиционные аргументы, даже когда их нет в сигнатуре; старый код исследует их этими функциями. Именованный quantity учитывает заполненный предыдущий default, как позиционная раскладка. Поэтому func_num_args — не универсальный способ выяснить, какие именно имена написал вызывающий. Для нового API яснее явные параметры и ...$rest.

func_num_args, func_get_arg и func_get_args допустимы и внутри замыканий. Индекс func_get_arg должен соответствовать переданному аргументу; проверяйте количество. Неожиданные именованные поля variadic не равнозначны обычным позиционным аргументам этой группы функций — явно исследуйте сам variadic-массив.

С PHP 8.3 замыкания, полученные из магических методов, поддерживают именованные аргументы. Это версионный нюанс динамического API; для совместимости с 8.2 не полагайтесь на него и предпочитайте явные методы с документированной сигнатурой. Подробнее — магические методы.

Мини-практика: какой обработчик выбрать?
  • Нужно убрать пробелы у каждого SKU: маленький fn или trim(...) как callable.
  • Нужно сохранить префикс магазина, заданный сейчас: замыкание use($prefix) или fn с захватом значения.
  • Нужно изменить внешний счётчик при каждом вызове: явное use(&$count), либо проще обычный цикл.
  • Нужен уже существующий метод с настройками объекта: $client->method(...), с учётом срока жизни клиента.
  • Нужно принимать только фиксированные поля API: обычная сигнатура и проверка данных; variadic не заменяет схему.

Функция может быть значением

Наглядная модель

Замыкание берёт нужную настройку с собой

Хотим подготовить правило скидки и позже применить его к нескольким товарам. Правилу нужна ставка, известная при его создании.

  1. Создать правило: скидка 10%

    Окружающий код передаёт ставку в замыкание через use.

  2. Сохранить вызываемое правило

    Вместе с функцией доступно захваченное значение ставки.

  3. Применить к цене 1000

    Функция принимает новую цену и возвращает 900.

Захват по значению сохраняет значение на момент создания. Захват по ссылке связывает с изменяемой переменной. Это осознанный выбор: правило скидки обычно удобнее делать независимым от случайных последующих изменений.

Сортировке не нужно знать все варианты товаров: она принимает правило сравнения. Фильтр получает правило отбора. Замыкание — функция, которую можно сохранить в переменную или передать как callback. В отличие от просто именованной функции оно может захватить данные окружающей области. Так обработчик знает ставку скидки или валюту без глобальных переменных.

Фабрика ниже получает процент сейчас и возвращает функцию для будущих цен. У неё два разных return. Внешний отдаёт правило, внутренний — рассчитанную цену. : Closure обозначает возвращаемую функцию, : int — цену в копейках. Сначала обычная запись function/use; сокращение fn появится дальше.

Создать два независимых правила скидки и использовать ценуСамостоятельный пример
<?php
function discountCalculator(int $percent): Closure {
    if ($percent < 0 || $percent > 100) {
        throw new InvalidArgumentException('Процент вне диапазона');
    }
    return function (int $kopecks) use ($percent): int {
        $discount = intdiv($kopecks * $percent, 100);
        return $kopecks - $discount;
    };
}
$springPrice = discountCalculator(15);
$regularPrice = discountCalculator(0);
$price = $springPrice(10000);
echo 'Со скидкой: ', $price, ' коп.', "\n";
echo 'Со скидкой и доставкой 500: ', $price + 500, ' коп.', "\n";
echo 'Без скидки: ', $regularPrice(10000), ' коп.', "\n";
Ожидаемый результат
Со скидкой: 8500 коп.
Со скидкой и доставкой 500: 9000 коп.
Без скидки: 10000 коп.

DiscountCalculator(15) проверяет процент и возвращает Closure со снимком 15 в springPrice. Цены в этот момент ещё нет. SpringPrice(10000) вызывает уже эту функцию: intdiv(10000*15,100)=1500, внутренний return даёт 8500. Оно сохраняется в price и участвует в следующем расчёте 8500+500. Другой экземпляр хранит 0 и возвращает 10000. Intdiv — целочисленное деление: здесь договор округляет скидку вниз для неотрицательных копеек; реальные правила округления выбирают отдельно.

Значение use и ссылка use(&...)

Намеренно следить за общей настройкойСамостоятельный пример
<?php
$limit = 10;
$snapshot = function () use ($limit) { return $limit; };
$live = function () use (&$limit) { return $limit; };
$limit = 20;
echo 'Лимит старого правила: ', $snapshot(), "\n";
echo 'Текущий общий лимит: ', $live(), "\n";
$requested = 15;
if ($requested <= $live()) { echo "Заявка на 15 принята текущим правилом\n"; }
Ожидаемый результат
Лимит старого правила: 10
Текущий общий лимит: 20
Заявка на 15 принята текущим правилом

Snapshot сохранил число 10; live через use(&$limit) сохраняет связь с внешней переменной. Поэтому после присваивания 20 они возвращают разные числа. Return live() поступает прямо в сравнение 15<=20 и разрешает сообщение. Ссылка полезна только при намеренно общей изменяемой настройке; для независимых правил используйте снимки. Пустые скобки означают, что новых аргументов у этих вызовов нет.

Чистый callback легче менять и проверять

Для следующего сравнения параметры a и b — по одной полной записи товара. Первый оператор <=> сравнивает остатки в обратном порядке, то есть по убыванию. Короткий ?: использует его, если он не 0; при равных остатках берёт сравнение артикулов. Return стрелки уходит алгоритму usort. После всех сравнений изменён products, а итоговые SKU выводятся через array_column и implode.

Сортировка по остатку и артикулуСамостоятельный пример
<?php
declare(strict_types=1);

$products = [
    ['sku' => 'B', 'stock' => 2],
    ['sku' => 'A', 'stock' => 2],
    ['sku' => 'C', 'stock' => 0],
];
usort($products, static fn (array $a, array $b): int =>
    ($b['stock'] <=> $a['stock']) ?: ($a['sku'] <=> $b['sku'])
);
echo implode(',', array_column($products, 'sku')), "\n";
Ожидаемый результат
A,B,C

Компаратор возвращает отрицательное, ноль или положительное число, а не bool. Сначала убывание остатка, при равенстве артикул дает предсказуемый порядок. usort изменяет массив и перенумеровывает ключи; если ключи — ID, нужен uasort либо явный список ID.

fn удобно для одного выражения, оно автоматически захватывает используемые внешние переменные по значению. Для ветвлений, нескольких операторов и сложной диагностики возьмите обычное function. Не превращайте короткий callback в мини-контроллер с SQL и отправкой писем: массовая операция будет вызывать его много раз, и потерявшаяся ошибка создаст частичные побочные эффекты.

Где заканчивается удобство

Цепочка array_filter→array_map→array_values создает промежуточные коллекции. Для небольшого каталога ясность важнее микросекунд. Для миллионов строк один foreach или генератор уменьшает пиковую память. Сравнивайте на объеме ваших данных и учитывайте чтение файла/базы. Не объявляйте замыкания медленными по одному микротесту; частый сетевой вызов внутри callback почти всегда важнее его формы.

Попробуйте сами. В callback нужно посчитать сумму и вернуть измененный список. Стоит ли захватывать $total по ссылке?

Разбор решения

Можно, но это скрытое состояние. Если задача простая, явный foreach соберет список и сумму в одном месте. Если нужна только сумма, array_reduce с начальным 0 или обычный цикл выражают контракт лучше.

Closure: готовое действие, которое можно хранить и передавать

В отчёте одна и та же строка заказа понадобится сейчас и позже, при экспорте. Можно сохранить само действие в переменной, а номер заказа передать при вызове. Объект Closure — форма, в которой PHP хранит анонимную функцию. Это встроенный класс, доступный без Composer. Если переменная и return ещё незнакомы, начните с объявления и вызова функции, затем вернитесь к самому первому примеру замыкания.

Код ещё не выполнялся → действие сохранено → получен результат
  1. function ($number) { ... } описывает действие
  2. $label = ... сохраняет объект Closure
  3. $label(41) запускает тело с number = 41
  4. return возвращает строку; echo печатает её
Два вызова одного сохранённого действияСамостоятельный пример
<?php
$label = function ($number) {
    return 'Заказ №' . $number;
};
echo $label(41), "\n";
echo $label(52), "\n";
var_dump($label instanceof Closure);
Ожидаемый результат
Заказ №41
Заказ №52
bool(true)

function без имени создаёт анонимную функцию; $number — её входной параметр. Точка соединяет строки, return отдаёт результат. Скобки после переменной выполняют вызов. instanceof задаёт вопрос о классе объекта; true означает «да». У объекта есть тело функции, а у переменной — возможность обратиться к этому объекту. Сохранение не создаёт поток или фоновую задачу.

Closure — final-класс: наследовать его нельзя, а его закрытый конструктор запрещает new Closure. Создание происходит через синтаксис функции, fn или преобразование callable. Произвольные свойства вроде $label->title к Closure добавить нельзя: PHP выдаст Error. Настройки сохраняют через use или в собственном классе. Объект вызывается как функция: $label(41). Техническая совместимость с __invoke не означает, что нужно вручную вызывать этот магический метод: движок исполняет Closure своим механизмом. Для собственного вызываемого объекта с настройками подходит __invoke и явный класс.

Нужное действиеЗаписьКогда появилось
Описать анонимную функциюfunction ($x) { return $x; }PHP 5.3
Короткое выражение с захватом значенийfn($x) => $x + $offsetPHP 7.4
Получить Closure для готовой функцииClosure::fromCallable('trim')PHP 7.1
Получить тот же вид вызываемого значения синтаксисомtrim(...)PHP 8.1
Внутри анонимной функции получить её самуClosure::getCurrent()PHP 8.5

Версия появления механизма и минимальная версия всего файла — разные вещи. Сами первые примеры ниже простые; типизированные свойства требуют PHP 7.4+, а уроки getCurrent явно помечены PHP 8.5+. Общая ветка мануала ориентирована на PHP 8.2+, кроме отдельно обозначенных новых возможностей.

Четыре детали замыкания: тело, захват, $this и область доступа

Пусть действие рассчитывает подпись посылки. Его текст — тело функции. Префикс из use — захваченное значение. Конкретная посылка в $this — привязанный объект. Право обратиться к private/protected задаётся областью класса. Последние две детали независимы: знать, какой объект передан, ещё не означает получить все права его класса.

ДетальЧто хранит / определяетЧто её меняет
ТелоНаписанный алгоритмДругое объявление функции
use ($prefix)Значение при создании; use (&$x) — общую ссылкуНовый вызов фабрики либо явно выбранное общее состояние
$thisКонкретный объектbind/bindTo в возвращаемом Closure, call на время вызова
Class scopeВидимость членов класса; контекст selfnewScope в bind/bindTo; класс объекта при call
Один алгоритм, два объекта, один захваченный префиксСамостоятельный пример
<?php
class Parcel { public $number; }
$first = new Parcel;
$first->number = 41;
$second = new Parcel;
$second->number = 52;
$prefix = 'Посылка №';
$template = function () use ($prefix) {
    return $prefix . $this->number;
};
$firstLabel = $template->bindTo($first, null);
$secondLabel = $template->bindTo($second, null);
echo $firstLabel(), "\n", $secondLabel(), "\n";
Ожидаемый результат
Посылка №41
Посылка №52

Здесь известное не-static тело и пользовательские объекты с public-свойством: обе привязки допустимы. null вторым аргументом не выдаёт права класса Parcel. Объекты не клонируются; обе новые функции имеют одинаковый текст, но разный $this. Исходный $template по-прежнему без объекта. В библиотеке с неизвестным входом результат привязки надо проверить на null; контракт показан ниже.

Строка "static" во втором аргументе bindTo — специальное значение «сохранить прежнюю область класса». Она не равна объявлению static function, не привязывает автоматически класс нового объекта и не включает позднее статическое связывание. При null-объекте нет $this; при отсутствии пользовательской области нельзя рассчитывать на доступ к private. Внутренняя служебная область PHP для некоторых сочетаний — деталь реализации, её имя не нужно использовать как разрешение доступа.

Привязка не добавляет метод к классу и не заменяет импорт переменных через use. Обычный параметр функции или явно переданный объект часто понятнее: $label($parcel) не требует менять контекст. К bind переходят, когда API доверенных callback уже устроен вокруг $this, либо при исследовании/тестировании собственного объекта.

Как выбрать метод Closure и не перепутать подготовку с выполнением

ПотребностьИнструментРезультат сейчас
Сохранить готовый метод/функцию как действиеClosure::fromCallableClosure; тело не выполняется
Подготовить действие с другим $this/scopeClosure::bindTo или Closure::bindНовый Closure либо null; тело не выполняется
Один раз выполнить с данным объектомClosure::callРезультат тела, вызов уже состоялся
Получить текущую анонимную функцию изнутриClosure::getCurrentТот же исполняющийся Closure; сам метод не делает рекурсию
Создать Closure вручную через newClosure::__constructЗапрещено; используйте синтаксис функции

Если контекст уже правильный, вызывайте $callback($value). Дополнительные обёртки увеличивают число понятий и мест, где можно потерять аргументы; обещать ускорение без измерения реального сценария нельзя. Для нескольких повторных вызовов допустимую привязку можно подготовить один раз, а не делать bind в каждой итерации. Но callback, обращающийся к базе или сети, обычно тратит время прежде всего на саму операцию.

Сигнатура callable у параметра допускает несколько форм: имя функции, массив объекта и имени метода, Closure или объект с __invoke. Сигнатура Closure требует именно объект этого класса. Преобразование в Closure нормализует форму действия, но не проверяет бизнес-данные и не делает чужой код безопасным. Формы вызова подробно разобраны в справочнике работы с функциями.

Хранить функцию в свойстве — ещё не добавить метод

Экспортёру нужен настраиваемый формат строки. Объявим у него свойство типа Closure и положим туда уже выбранное действие. Тип callable для свойства PHP не поддерживает: допустимость строкового/массивного callback зависит от области доступа. Нормализованный Closure можно хранить и вызывать явно.

Экспортёр вызывает действие из свойстваСамостоятельный пример
<?php
declare(strict_types=1);

class Exporter
{
    public Closure $format;
}
$exporter = new Exporter;
$exporter->format = static fn($number) => 'Заказ №' . $number;
echo ($exporter->format)(41), "\n";
var_dump(method_exists($exporter, 'format'));
Ожидаемый результат
Заказ №41
bool(false)

Внешние скобки сначала читают свойство, следующие вызывают хранящуюся в нём функцию. $exporter->format(41) означает поиск метода и здесь не подходит. public в этом учебном шаге делает замену видимой; в настоящем экспортёре обычно принимают callback через конструктор и хранят private. __call ради этой одной операции не нужен.

Для задания в очереди сохранить данные, а не ClosureСамостоятельный пример
<?php
declare(strict_types=1);

$format = static fn($number) => 'Заказ №' . $number;
try {
    serialize($format);
} catch (Exception $error) {
    echo "Closure не сериализуется\n";
}
$job = ['action' => 'order-label', 'order_id' => 41];
$actions = ['order-label' => $format];
echo $actions[$job['action']]($job['order_id']), "\n";
Ожидаемый результат
Closure не сериализуется
Заказ №41

Встроенный serialize не сериализует Closure и выбрасывает Exception. Здесь задание — локальный заранее известный массив. В очереди сохраняют проверенные данные и идентификатор разрешённой операции; после чтения проверяют схему, наличие action в allowlist, права и тип order_id. Имя PHP-функции из внешнего JSON нельзя прямо превращать в callback. Пример не содержит очереди, хранения или доставки.

Сторонняя библиотека сериализации функций имеет отдельный формат и модель доверия; наличие такой библиотеки не отменяет ограничение встроенного PHP. Для передачи между HTTP-запросами объект функции сам не сохраняется. Нужны данные в хранилище и код, который заново выберет разрешённое действие.

Копия Closure и срок жизни связанных объектов

clone отделяет собственный счётчик, но не захваченный объектСамостоятельный пример
<?php
declare(strict_types=1);

$journal = new stdClass;
$journal->count = 0;
$next = static function () use ($journal) {
    static $local = 0;
    ++$journal->count;
    return ++$local;
};
echo $next(), "\n";
$copy = clone $next;
echo $next(), ':', $copy(), "\n";
echo $journal->count, "\n";
var_dump($copy === $next);
Ожидаемый результат
1
2:2
3
bool(false)

У этой настоящей анонимной функции clone создаёт отдельный Closure и копирует текущее собственное static-состояние: оба счётчика продолжили с 1 до 2. Захваченный объект журнала общий, поэтому его count стал 3. Это не глубокое копирование. Захват через use (&$variable) также оставляет общую ссылку. Для Closure, полученного из именованной функции, её static-переменные принадлежат исходной функции; не переносите на такой адаптер вывод об отдельном счётчике.

Сохранённый метод удерживает объект до освобождения callbackСамостоятельный пример
<?php
declare(strict_types=1);

class Labeler
{
    public function text() { return 'Заказ'; }
}
$labeler = new Labeler;
$observer = WeakReference::create($labeler);
$callback = Closure::fromCallable([$labeler, 'text']);
unset($labeler);
var_dump($observer->get() !== null);
echo $callback(), "\n";
unset($callback);
var_dump($observer->get() === null);
Ожидаемый результат
bool(true)
Заказ
bool(true)

WeakReference наблюдает объект, не удерживая его. Единственная сильная связь после unset($labeler) здесь находится в callback. Когда удалён и callback, объект освобождён. В реальном приложении могут быть другие ссылки и циклы; пример не обещает освобождение сразу после любого unset. Для длительно живущего worker не храните ненужные callback с большим окружением.

Равенство исходного алгоритма не означает тождество объектов. Для снятия зарегистрированного callback надёжно сохранить тот же экземпляр, а не заново написать похожую функцию. static function запрещает $this, но всё ещё может захватывать объект через use: «static» не обещает отсутствия связей или памяти. Отдельные правила сравнения объектов — в классах и объектах.

Исследовать функцию, не выполняя её и не публикуя окружение

Узнать число обязательных параметров и привязкуСамостоятельный пример
<?php
declare(strict_types=1);

$format = static function ($number, $prefix = 'Заказ') {
    return $prefix . ' №' . $number;
};
$info = new ReflectionFunction($format);
echo 'Обязательных: ', $info->getNumberOfRequiredParameters(), "\n";
echo 'Всего: ', $info->getNumberOfParameters(), "\n";
var_dump($info->getClosureThis() === null);
var_dump($info->isClosure());
Ожидаемый результат
Обязательных: 1
Всего: 2
bool(true)
bool(true)

ReflectionFunction получает описание действия. Он не вызывает тело и не показывает историю прежних вызовов. Один параметр обязателен, второй имеет значение по умолчанию; у static-функции нет $this. Для проверки совместимости конкретного callback одних количеств недостаточно: важны типы, ссылки, variadic и смысл аргументов.

Начиная с PHP 8.4 диагностическое представление Closure через __debugInfo включает дополнительные имя, файл и строку. Поэтому полный var_dump callback нельзя бездумно отдавать клиенту: в нём может оказаться локальный путь, захваченное состояние и связанный объект. Для журнала выберите только нужные признаки, например число параметров и собственную безопасную метку операции. Reflection не заменяет лог или debugger для наблюдения потока исполнения.

Область доступа не является песочницей или авторизацией

bind/call работают с полномочиями кода внутри процесса PHP. Если доверенной функции выдать scope собственного класса, она сможет обращаться к его private/protected как код этой области. Нельзя считать private защитой от уже исполняемого недоверенного PHP. Но из этого не следует, что в прикладном коде надо читать любые чужие private-поля: такое связывание привязывает вас к внутреннему устройству библиотеки и может обходить её проверки.

Как выдать одно разрешённое действие
  1. Класс определяет приватную реализацию
  2. Публичный метод осознанно создаёт Closure для этой операции
  3. Вызывающий получает готовое действие с проверенными параметрами
  4. Другие операции класса автоматически не становятся публичными

Практический выбор: для собственного сервиса добавьте понятный public-метод или верните callback, созданный внутри допустимой области. Для стороннего объекта используйте документированный API или адаптер. Не принимайте имя класса для newScope, имя метода и исполняемый callback прямо из HTTP-запроса. Callback-реестр формируется кодом из разрешённых операций; проверка пользователя и права на конкретный заказ выполняются отдельно.

Упражнение: экспортёр должен форматировать номер, но не менять заказ. Передайте ему Closure от разрешённого метода форматирования. Затем попробуйте получить тот же private-метод снаружи: ожидается TypeError на создании. Готовое решение — второй пример Closure::fromCallable. Зависимость от private лучше заменить явным контрактом, если внешним разработчикам нужно расширять сервис.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

Closure::__constructПочему new Closure запрещён и как получить объект функции.

PHP 5.3+ · Core

private function Closure::__construct()
Параметр / результатДоговор
ПараметрыНет; пользовательский вызов конструктора не предусмотрен.
Созданиеnew Closure вызывает Error. Используйте function/fn, fromCallable или first-class callable.
Возвращаемое значениеУ этого конструктора нет пользовательского результата; объект создаёт языковой механизм.

Потребность — получить сохраняемое действие, а не вручную заполнить внутренности функции. Класс существует для типа и методов управления Closure; его закрытый конструктор не является забытым публичным способом создания.

Создать действием function и увидеть отказ newСамостоятельный пример
<?php
$greet = function ($name) { return 'Здравствуйте, ' . $name; };
echo $greet('Анна'), "\n";
try {
    new Closure;
} catch (Error $error) {
    echo "Используем function, а не new Closure\n";
}
Ожидаемый результат
Здравствуйте, Анна
Используем function, а не new Closure

Первая строка создаёт объект функции без вызова её тела. catch нужен здесь только для демонстрации запрета; в рабочем коде не нужно сначала пробовать заведомо недопустимый конструктор.

Фабрика подписей для двух видов документовСамостоятельный пример
<?php
function makeLabel($prefix)
{
    return function ($number) use ($prefix) {
        return $prefix . ' №' . $number;
    };
}
$orderLabel = makeLabel('Заказ');
$invoiceLabel = makeLabel('Счёт');
echo $orderLabel(41), "\n", $invoiceLabel(8), "\n";
Ожидаемый результат
Заказ №41
Счёт №8

makeLabel выполняется сразу и возвращает другую функцию. use сохраняет prefix выбранного вызова фабрики; number появится только при последующем вызове. Два документа используют общий алгоритм без класса, наследования или глобальной настройки. Для одного постоянного префикса такая фабрика необязательна.

Класс final нельзя расширить своим конструктором. Если действию нужны много настроек и отдельные операции, создайте обычный класс с __invoke. Если нужно только превратить готовый метод в сохраняемое значение, выберите fromCallable; тело метода при преобразовании не исполняется.

Сравните: Closure::fromCallable, Closure::bindTo. Модель контекста: тело, объект и область доступа.

Closure::bindСтатическая форма: подготовить функцию с явными объектом и областью доступа.

PHP 5.4+ · Core

public static function Closure::bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure
Параметр / результатДоговор
$closureИменно Closure, не строковое имя функции или произвольный callable. При необходимости преобразуйте допустимый callable заранее.
$newThisОбъект для $this либо null без объекта. Объект не клонируется. Static-замыкание не принимает объект.
$newScope = "static"Класс видимости: имя пользовательского класса, объект (используется его класс), null без прав пользовательского класса; "static" сохраняет прежнюю область.
Возврат ?ClosureНовая функция с тем же телом и захватами; при недопустимой привязке null и диагностическое сообщение. Исходная функция не меняется.

Два одинаковых шаблона доставки должны читать данные разных посылок. bind получает исходную функцию отдельным первым аргументом. Оператор :: выбирает статический метод класса Closure: собственный объект Closure как получатель слева здесь не нужен.

Подготовить подпись и вызвать её отдельноСамостоятельный пример
<?php
class Parcel { public $number = 41; }
$template = function () { return 'Посылка №' . $this->number; };
$label = Closure::bind($template, new Parcel, null);
if ($label === null) {
    throw new RuntimeException('Не удалось подготовить подпись');
}
echo "Подпись подготовлена\n";
echo $label(), "\n";
Ожидаемый результат
Подпись подготовлена
Посылка №41

bind ещё не печатает и не выполняет тело. new Parcel создаёт учебный объект, который будет доступен как $this при вызове label. null в newScope подходит для public-свойства. if отделяет отсутствие функции от последующего результата её работы.

Объект один, но права области доступа разныеСамостоятельный пример
<?php
declare(strict_types=1);

class ShippingRule
{
    private int $baseMinor = 300;
}
$rule = new ShippingRule;
$calculate = function ($extraMinor) { return $this->baseMinor + $extraMinor; };
$publicOnly = Closure::bind($calculate, $rule, null);
try {
    $publicOnly(50);
} catch (Error $error) {
    echo "Без области класса private недоступен\n";
}
$trusted = Closure::bind($calculate, $rule, ShippingRule::class);
echo $trusted(50), "\n";
Ожидаемый результат
Без области класса private недоступен
350

Привязка publicOnly допустима, но ошибка возникает позже при чтении private. Второй вариант осознанно выдаёт нашему учебному коду область ShippingRule. Это демонстрация двух осей контекста для собственного класса. Обычному клиенту доставки лучше предоставить public calculate(), который сохраняет её инварианты.

Static-функция может иметь область класса без $thisСамостоятельный пример
<?php
declare(strict_types=1);

class LabelRules { private const PREFIX = 'Заказ'; }
$readPrefix = static function () { return self::PREFIX; };
$reader = Closure::bind($readPrefix, null, LabelRules::class);
echo $reader(), "\n";
var_dump((new ReflectionFunction($reader))->getClosureThis() === null);
Ожидаемый результат
Заказ
bool(true)

self относится к области класса; $this относится к объекту. Static-замыкание не получает объекта, но может иметь область доступа. private-константа в этом примере принадлежит нашему классу. Для обычного кода предпочтительнее явно объявить публичную операцию, чем читать закрытую константу связыванием.

Ошибочный тип аргумента даёт TypeError. Неизвестное имя newScope может запустить autoload; если класс не найден, результат null с предупреждением. Нельзя выдавать область встроенного класса вроде stdClass. Нельзя привязать объект к static-замыканию или метод к объекту, не принадлежащему классу объявления метода/его подклассу. Нельзя снять $this у метода либо у анонимной функции, которая его использует. Scope Closure из существующей функции/метода нельзя произвольно менять. Сам факт instanceof Closure не обещает произвольную перепривязку.

Для той же операции в форме метода у исходной функции используйте bindTo. Для единственного немедленного запуска call проще, но он выбирает scope по классу объекта, а не принимает отдельный newScope. Захваченные use-значения не подменяются аргументами bind.

Сравните: Closure::bindTo, Closure::call, Closure::fromCallable. Модель контекста: тело, объект и область доступа.

Closure::bindToПолучить новую версию функции, не изменяя исходную.

PHP 5.4+ · Core

public function Closure::bindTo(?object $newThis, object|string|null $newScope = "static"): ?Closure
Параметр / результатДоговор
$newThisОбъект для $this либо null без объекта. Объект не клонируется. Static-замыкание не принимает объект.
$newScope = "static"Класс видимости: имя пользовательского класса, объект (используется его класс), null без прав пользовательского класса; "static" сохраняет прежнюю область.
Возврат ?ClosureНовая функция с тем же телом и захватами; при недопустимой привязке null и диагностическое сообщение. Исходная функция не меняется.

Уже есть Closure от одного экспортёра. Хотим подготовить второй вариант с другим экземпляром той же настройки. bindTo вызывается через -> у исходного объекта функции и возвращает новый. Если выбросить возвращаемое значение, исходная функция останется прежней.

Две настройки, исходный callback остаётся исходнымСамостоятельный пример
<?php
class Labeler
{
    private $prefix;
    public function __construct($prefix) { $this->prefix = $prefix; }
    public function makeLabel()
    {
        return function ($number) { return $this->prefix . ' №' . $number; };
    }
}
$orders = new Labeler('Заказ');
$invoices = new Labeler('Счёт');
$original = $orders->makeLabel();
$rebound = $original->bindTo($invoices);
echo $original(41), "\n";
echo $rebound(8), "\n";
echo $original(52), "\n";
Ожидаемый результат
Заказ №41
Счёт №8
Заказ №52

В makeLabel обычная анонимная функция получает $this и область Labeler автоматически. newScope по умолчанию сохраняет именно эту область; она подходит второму экземпляру Labeler. Конструктор и private здесь пройдены ранее в главе классов. Практический альтернативный вариант проще: $invoices->makeLabel() создаёт нужное действие напрямую.

Убрать область класса у преобразования в публичные данныеСамостоятельный пример
<?php
declare(strict_types=1);

class CatalogItem
{
    public string $title = 'Кабель';
    private int $draftPrice = 120;
    public function makePublicView(): Closure
    {
        $collect = static function ($item) { return get_object_vars($item); };
        return $collect->bindTo(null, null);
    }
}
$item = new CatalogItem;
$view = $item->makePublicView();
echo implode(',', array_keys($view($item))), "\n";
Ожидаемый результат
title

У collect изначально область CatalogItem, даже несмотря на static. Два null убирают и объект, и права области. get_object_vars тогда видит только доступные public-свойства. Это опыт над доступностью, не готовый сериализатор API: новый public-field может быть нежелателен снаружи, поэтому стабильный HTTP-ответ лучше собирать явным allowlist ключей.

Перехватить учебное предупреждение и проверить nullСамостоятельный пример
<?php
declare(strict_types=1);

class Parcel {}
$callback = static function () { return 'готово'; };
$levels = [];
set_error_handler(static function ($severity, $message) use (&$levels) {
    $levels[] = $severity;
    return true;
});
try {
    $rebound = $callback->bindTo(new Parcel);
} finally {
    restore_error_handler();
}
var_dump($rebound === null);
var_dump($levels === [E_WARNING]);
echo $callback(), "\n";
Ожидаемый результат
bool(true)
bool(true)
готово

Намеренно неверная попытка проверена на PHP 8.5.11. Handler нужен только для детерминированного учебного опыта и всегда снимается в finally. Исходная static-функция осталась работоспособной. В приложении исправляют неверный контракт до вызова: выбирают функцию с параметром или не-static callback; не подавляют все предупреждения глобально.

Ошибочный тип аргумента даёт TypeError. Неизвестное имя newScope может запустить autoload; если класс не найден, результат null с предупреждением. Нельзя выдавать область встроенного класса вроде stdClass. Нельзя привязать объект к static-замыканию или метод к объекту, не принадлежащему классу объявления метода/его подклассу. Нельзя снять $this у метода либо у анонимной функции, которая его использует. Scope Closure из существующей функции/метода нельзя произвольно менять. Сам факт instanceof Closure не обещает произвольную перепривязку.

Если задача лишь получить копию без смены контекста, существует clone. Это отдельный объект Closure, но не глубокая копия захваченного графа. Если нужен null-$this у функции, в теле которой он используется, простое bindTo(null) не превращает алгоритм в независимый — перепишите тело с явным параметром.

Сравните: Closure::bind, Closure::call, Closure::fromCallable. Модель контекста: тело, объект и область доступа.

Closure::callВременно выбрать объект и сразу получить результат выполнения.

PHP 7.0+ · Core

public function Closure::call(object $newThis, mixed ...$args): mixed
Параметр / результатДоговор
$newThisОбязательный объект для одного вызова; null нельзя. Class scope на время вызова берётся от его класса.
...$argsНоль или больше аргументов тела, передаваемых по значению. Это не свойства объекта. Имена должны соответствовать сигнатуре callback; ссылочную мутацию исходной переменной call не передаёт.
mixedРезультат тела: строка, число, массив, объект, null и т.д.; не новый Closure. Исключение из тела проходит наружу.
Недопустимая привязкаНа PHP 8.5.11 warning и null; TypeError при неверном типе newThis. null может быть и законным результатом тела — по нему одному успех не определяют.

Нужно один раз получить расчёт на двух наших учебных правилах доставки. call совмещает временную привязку и вызов. Он не оставляет исходную функцию с новым $this и не требует сохранять промежуточную копию.

Вычислить результат сейчасСамостоятельный пример
<?php
class Parcel { public $weight = 3; }
$cost = function ($pricePerUnit) { return $this->weight * $pricePerUnit; };
echo $cost->call(new Parcel, 20), "\n";
var_dump((new ReflectionFunction($cost))->getClosureThis() === null);
Ожидаемый результат
60
bool(true)

20 попадает в pricePerUnit, объект Parcel — в $this. Временный контекст используется только во время call. После него исходная функция снова без объекта. При обычном public-контракте функция calculate($parcel, $pricePerUnit) будет более явной.

Одно временное вычисление не меняет постоянную привязкуСамостоятельный пример
<?php
declare(strict_types=1);

class ShippingRule
{
    private int $baseMinor;
    public function __construct(int $baseMinor) { $this->baseMinor = $baseMinor; }
    public function calculation(): Closure
    {
        return function (int $extraMinor): int { return $this->baseMinor + $extraMinor; };
    }
}
$regular = new ShippingRule(300);
$express = new ShippingRule(700);
$calculate = $regular->calculation();
echo $calculate(50), "\n";
echo $calculate->call($express, 50), "\n";
echo $calculate(50), "\n";
Ожидаемый результат
350
750
350

Доплата в примере — уже проверенное целое число в минимальных единицах. Область call берётся от ShippingRule, поэтому private baseMinor доступен. Вычисление не списывает деньги и не меняет поля. Если тело меняет объект, его изменения сохранятся: временность относится к контексту функции, не к откату действий.

Различить исключение тела и особенности передачи скаляраСамостоятельный пример
<?php
declare(strict_types=1);

class Parcel {}
$accept = function (int $count): int { return $count; };
try {
    $accept('3');
} catch (TypeError $error) {
    echo "Прямой strict-вызов отклонил строку\n";
}
echo 'call: ', $accept->call(new Parcel, '3'), "\n";
$fail = function () { throw new DomainException('Нет тарифа'); };
try {
    $fail->call(new Parcel);
} catch (DomainException $error) {
    echo $error->getMessage(), "\n";
}
Ожидаемый результат
Прямой strict-вызов отклонил строку
call: 3
Нет тарифа

Файл содержит declare(strict_types=1). На PHP 8.5.11 внутренний вызов через call применяет скалярное приведение, в отличие от прямого вызова в strict-файле. Поэтому наличие int у параметра не заменяет проверку исходного HTTP-ввода. Пример фиксирует именно этот путь вызова, а не правило для всех посредников. Исключение, уже выброшенное телом, не превращается в null.

Для ссылочного аргумента сначала bindTo, затем прямой вызовСамостоятельный пример
<?php
declare(strict_types=1);

class Parcel {}
$increment = function (&$count) { ++$count; };
$count = 1;
$levels = [];
set_error_handler(static function ($severity, $message) use (&$levels) {
    $levels[] = $severity;
    return true;
});
try {
    $increment->call(new Parcel, $count);
} finally {
    restore_error_handler();
}
echo 'После call: ', $count, "\n";
var_dump($levels === [E_WARNING]);
$bound = $increment->bindTo(new Parcel, null);
$bound($count);
echo 'После прямого вызова: ', $count, "\n";
Ожидаемый результат
После call: 1
bool(true)
После прямого вызова: 2

На PHP 8.5.11 call выдаёт предупреждение о ссылочном параметре, тело меняет временное значение, а исходный count остаётся 1. Это не отказ выполнить всё тело: его другие эффекты тоже возможны. При прямом вызове подготовленного Closure переменная передаётся по контракту & и изменяется до 2. Если $this вообще не используется, привязка не нужна: достаточно $increment($count).

Нельзя использовать call для static-замыкания с объектом или ожидать чтения private встроенного класса: для таких привязок действуют ограничения Closure. Именованные аргументы допустимы по контракту вызова, но переименование параметра callback ломает код, использующий старое имя. Ссылочные параметры требуют прямого вызова с переменными; упаковка ссылок в массив и его распаковка в call не превращает ...args в ссылочный параметр.

Для отложенного действия выберите bindTo. Для единственного заранее известного public-метода объекта прямой вызов метода понятнее обоих вариантов. call не защищает код от исключений, тайм-аутов или побочных эффектов.

Сравните: Closure::bindTo, Closure::bind, Closure::fromCallable. Модель контекста: тело, объект и область доступа.

Closure::fromCallableСохранить уже существующее действие с проверкой доступности сейчас.

PHP 7.1+; first-class callable PHP 8.1+ · Core

public static function Closure::fromCallable(callable $callback): Closure
Параметр / результатДоговор
$callbackДоступная в текущей области функция, метод, вызываемый объект или Closure. Массив метода содержит объект/класс и имя.
Возврат ClosureВызываемое значение для выбранной операции; сама операция ещё не выполнена. Если передан уже Closure, PHP 8.5.11 возвращает тот же объект.
ОшибкаTypeError, если форма недопустима или метод недоступен в текущей области. Ошибки тела возникают при последующем вызове.

Система выгрузки принимает один вид действия — Closure, а разработчик передаёт готовую функцию или метод. fromCallable нормализует форму и проверяет доступность в месте получения. Это не частичное применение параметров: новые параметры алгоритма всё равно передаются при вызове, либо сохраняются отдельной обёрткой с use.

Выбрать существующую функцию без немедленного исполненияСамостоятельный пример
<?php
function orderLabel($number) { return 'Заказ №' . $number; }
$format = Closure::fromCallable('orderLabel');
echo "Формат выбран\n";
echo $format(41), "\n";
$short = orderLabel(...);
echo $short(52), "\n";
Ожидаемый результат
Формат выбран
Заказ №41
Заказ №52

Первый способ существует с PHP 7.1, запись с буквальными (...) — с PHP 8.1. Эти три точки без переменной создают first-class callable; $format(...$values) означало бы распаковку массива и выполнение. Не передавайте результат orderLabel(41) вместо имени: это уже строка подписи, а не callback.

Класс осознанно отдаёт одну приватную операциюСамостоятельный пример
<?php
declare(strict_types=1);

class OrderLabels
{
    private function format(int $number): string { return 'Заказ №' . $number; }
    public function formatter(): Closure
    {
        return Closure::fromCallable([$this, 'format']);
    }
}
$labels = new OrderLabels;
$format = $labels->formatter();
echo $format(41), "\n";
try {
    Closure::fromCallable([$labels, 'format']);
} catch (TypeError $error) {
    echo "Снаружи новое разрешение не получено\n";
}
Ожидаемый результат
Заказ №41
Снаружи новое разрешение не получено

Внутри formatter метод format доступен; класс намеренно отдаёт готовую возможность его вызова. Снаружи полученный Closure продолжает работать. Но попытка заново получить private-метод по массиву снаружи запрещена. Это не смена visibility метода и не авторизация пользователя: выдачу действия контролирует владелец API.

Добавить настройку через обёртку, а не через fromCallableСамостоятельный пример
<?php
declare(strict_types=1);

function documentLabel(string $prefix, int $number): string
{
    return $prefix . ' №' . $number;
}
$original = Closure::fromCallable('documentLabel');
$prefix = 'Счёт';
$invoiceLabel = static function (int $number) use ($original, $prefix): string {
    return $original($prefix, $number);
};
echo $invoiceLabel(8), "\n";
var_dump(Closure::fromCallable($original) === $original);
Ожидаемый результат
Счёт №8
bool(true)

Обёртка хранит prefix и предъявляет наружу только number. use относится к объявлению function, его нельзя дописать к вызову fromCallable. fromCallable не обещает новый независимый объект для уже готового Closure; для сознательного копирования существует clone с отдельными ограничениями состояния.

Экспортёр принимает callable, хранит ClosureСамостоятельный пример
<?php
declare(strict_types=1);

class Exporter
{
    private Closure $format;
    public function __construct(callable $format)
    {
        $this->format = Closure::fromCallable($format);
    }
    public function row(int $number): string
    {
        return ($this->format)($number);
    }
}
$exporter = new Exporter(static fn(int $id): string => 'Заказ №' . $id);
echo $exporter->row(41), "\n";
Ожидаемый результат
Заказ №41

callable допустим у входного параметра; Closure допустим у свойства. Скобки вокруг $this->format читают свойство перед вызовом. Контракт row обещает строку: произвольный callable, возвращающий другой несовместимый тип, нарушит его при исполнении, поэтому документация конструктора должна также описывать параметры и результат callback. Сам тип callable форму этого результата не кодирует.

В PHP 8 нестатический метод нельзя получить как callable через одно имя класса без объекта. Полностью квалифицированные имена важны в namespace; use-псевдоним не переписывает произвольную строку. Контекстные старые строки вроде self::method устарели с PHP 8.2; предпочитайте first-class callable, создаваемый внутри разрешённой области.

Closure от готового метода сохраняет связь с этим методом и объектом. Это не переписанное анонимное тело, которому можно произвольно менять class scope. Используйте явную обёртку для нового алгоритма; для получения нового метода другого объекта создайте его callable заново. Реестр callback должен состоять из доверенного кода, а не произвольных имён из POST.

Сравните: Closure::__construct, Closure::bindTo, Closure::getCurrent. Модель контекста: тело, объект и область доступа.

Closure::getCurrentPHP 8.5+: получить исполняющееся замыкание для самообращения.

PHP 8.5+ · Core

public static function Closure::getCurrent(): Closure
Параметр / результатДоговор
ПараметрыНет. Вызывать непосредственно внутри тела анонимной/стрелочной функции.
Возврат ClosureТот самый исполняющийся объект функции, не копия и не результат его выполнения.
ErrorВне такого тела: на верхнем уровне, в обычной функции/методе, в том числе превращённом в Closure через fromCallable. Вызов из named helper не ищет внешнее замыкание в стеке.
ВерсияТолько PHP 8.5+. В более старой версии метода нет; используйте именованную функцию либо изученный use (&$self).

Рекурсивное замыкание обходит дерево категорий, но имени у его объявления нет. В PHP 8.5 getCurrent позволяет получить саму исполняющуюся функцию внутри её тела. Рекурсивный шаг всё равно пишем явно. Сначала опыт без дерева: посчитать до границы и остановиться.

PHP 8.5+: отсчёт с явной остановкойСамостоятельный пример
<?php
$countDown = function ($number) {
    if ($number <= 0) {
        return 'стоп';
    }
    $again = Closure::getCurrent();
    return $number . ' → ' . $again($number - 1);
};
echo $countDown(3), "\n";
Ожидаемый результат
3 → 2 → 1 → стоп

if ограничивает рекурсию: 3→2→1→0. getCurrent только возвращает функцию; $again($number - 1) вызывает её снова. return собирает строку при возвращении из вложенных вызовов. Вход фиксирован учебным примером; для произвольного внешнего числа нужна проверка диапазона. Для простого отсчёта цикл проще — рекурсия здесь объясняет механизм.

PHP 8.5+: посчитать узлы небольшого дерева категорийСамостоятельный пример
<?php
declare(strict_types=1);

$countNodes = static function (array $nodes, int $depth = 0): int {
    if ($depth > 3) {
        throw new DomainException('Слишком глубокое дерево');
    }
    $again = Closure::getCurrent();
    $total = 0;
    foreach ($nodes as $node) {
        ++$total;
        $total += $again($node['children'], $depth + 1);
    }
    return $total;
};
$tree = [
    ['title' => 'Кабели', 'children' => [
        ['title' => 'Силовые', 'children' => []],
        ['title' => 'Слаботочные', 'children' => []],
    ]],
    ['title' => 'Крепёж', 'children' => []],
];
echo $countNodes($tree), "\n";
Ожидаемый результат
4

Это уже известный локальный массив с одинаковой схемой children. foreach посещает каждый узел один раз; пустой children завершает ветку. depth ограничивает вложенность, но не количество соседних узлов и не общий размер данных. После JSON отдельно проверяют схему, число элементов, глубину и объём ответа. Для очень глубоких деревьев применяют явный стек; getCurrent не устраняет стоимость рекурсии.

PHP 8.5+: переименование внешней переменной не ломает самообращениеСамостоятельный пример
<?php
declare(strict_types=1);

$walk = function (int $remaining): int {
    if ($remaining === 0) { return 0; }
    $self = Closure::getCurrent();
    return 1 + $self($remaining - 1);
};
$saved = $walk;
$walk = static fn(int $remaining): int => 999;
echo $saved(3), "\n", $walk(3), "\n";
Ожидаемый результат
3
999

getCurrent связан с исполняющимся объектом, а не с текущим содержимым переменной walk. В старом варианте use (&$walk) рекурсивный вызов прочитал бы её новое содержимое. Возвращаемый Closure может удерживать всё своё окружение; сохранять его навсегда без необходимости не следует.

PHP 8.5+: обычный helper не получает внешний ClosureСамостоятельный пример
<?php
declare(strict_types=1);

function currentFromHelper(): Closure { return Closure::getCurrent(); }
$outer = static function (): void {
    try {
        currentFromHelper();
    } catch (Error $error) {
        echo "Helper — обычная функция\n";
    }
};
$outer();
$converted = Closure::fromCallable('currentFromHelper');
try {
    $converted();
} catch (Error $error) {
    echo "Преобразование не изменило тело исходной функции\n";
}
Ожидаемый результат
Helper — обычная функция
Преобразование не изменило тело исходной функции

Метод проверяет непосредственно текущую функцию, не ищет ближайшее замыкание выше в стеке. Адаптер named-функции через fromCallable также не превращает её тело в настоящий анонимный контекст. Вложенное анонимное тело, напротив, получит себя, а не outer. Для PHP до 8.5 весь этот файл непригоден без другого алгоритма.

Самообращение требуется только рекурсивной или самостоятельно перерегистрируемой функции. Для обычного форматирования getCurrent не нужен. Если алгоритм имеет устойчивое предметное имя, обычная именованная функция или метод остаётся удобнее. Практика: добавьте ограничение общего количества узлов к примеру дерева и отличите его от ограничения depth. Для этого нужен отдельный счётчик, передаваемый явно или осознанно захваченный по ссылке; одна проверка глубины широкое дерево не ограничивает.

Сравните: Closure::fromCallable, Closure::__construct. Модель контекста: тело, объект и область доступа.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.