Привет, Хабр.
Сегодня рассмотрим, как в PHP управлять контекстом замыканий: подменять $this, менять область видимости, получать доступ к приватным свойствам, оборачивать методы, реализовывать мини-AOP и использовать замыкания как ленивые фабрики в DI-контейнерах.
bind и bindTo
PHP представляет замыкания через специальный класс Closure, экземпляры которого можно модифицировать. У этого класса есть два метода:
public Closure::bindTo(?object $newThis, object|string|null $newScope = "static"): ?Closure public static Closure::bind(Closure $closure, ?object $newThis, object|string|null $newScope = "static"): ?Closure
Оба метода делают одно и то же: создают новое замыкание, которое выполняется с другим значением $this и другим объёмом видимости (scope), определяющим, к каким приватным/защищённым членам класс имеет доступ.
Если вы путаетесь: bindTo — это instance-метод, применяется к конкретному Closure. bind — статический метод, применяется к замыканию, переданному первым аргументом. Отличий по сути нет — дело вкуса.
Контекст $this
По умолчанию, если вы создаёте замыкание вне методов класса, внутри него нет $this. Но если вы создаёте его в контексте метода, $this определяется автоматически. Пример:
class Demo { public function makeClosure(): Closure { return function () { return $this; }; } } $demo = new Demo(); $closure = $demo->makeClosure(); var_dump($closure()); // object(Demo)
Теперь попробуем создать closure вне класса:
$closure = function () { return $this; }; $closure(); // Fatal error: Using $this when not in object context
Чтобы в таком случае привязать $this, мы можем использовать bindTo:
$bound = $closure->bindTo(new StdClass()); var_dump($bound()); // object(stdClass)
Всё просто: вручную подвязали $this к замыканию.
Доступ к приватным свойствам через scope binding
Это второй аргумент метода bind или bindTo. Он определяет, к каким приватным и защищённым членам классов замыкание получит доступ. Если scope не указан, используется ‘static’, то есть доступ будет только к публичному API.
Посмотрим:
class Secret { private string $value = 'classified'; } $secret = new Secret(); $closure = function () { return $this->value; }; $bound = Closure::bind($closure, $secret, 'Secret'); echo $bound(); // classified
Здесь мы получили доступ к приватному полю без всякой рефлексии. Важно: это не баг, это официально поддерживаемое поведение, хотя и потенциально опасное с точки зрения инкапсуляции.
Если передать ‘static’ как $scope, то доступ к приватным и защищённым членам будет невозможен:
$bound = Closure::bind($closure, $secret, 'static'); $bound(); // Fatal error
Привязка к null — отключение $this
Можно также привязать замыкание к null, чтобы убрать привязку к объекту и избавиться от $this вообще:
$closure = function () { return isset($this); }; $unbound = $closure->bindTo(null); var_dump($unbound()); // false
Примеры применения
AOP: обернуть метод до/после
Допустим, есть объект с методом, и хочется повесить на него логгирование:
class Service { public function execute($arg) { return "Result: " . $arg; } } $service = new Service(); $originalMethod = function (...$args) { return call_user_func_array([$this, 'execute'], $args); }; $wrapper = function (...$args) { echo "[LOG] before\n"; $result = call_user_func_array($this->original, $args); echo "[LOG] after\n"; return $result; }; $boundOriginal = Closure::bind($originalMethod, $service, Service::class); $proxy = new class($service, $boundOriginal, $wrapper) { public Closure $original; public Closure $wrapped; public function __construct($target, Closure $original, Closure $wrapper) { $this->original = $original; $this->wrapped = $wrapper->bindTo($this, self::class); } public function __call($name, $args) { if ($name === 'execute') { return ($this->wrapped)(...$args); } throw new \BadMethodCallException(); } }; echo $proxy->execute('foo'); // [LOG] before // [LOG] after // Result: foo
Создали мини-AOP, не меняя исходный класс и не прибегая к eval или рефлексии. Только Closure::bind().
DI-контейнер
Многие контейнеры в PHP (например, Laravel) позволяют регистрировать сервисы в виде замыканий. С bindTo можно удобно управлять контекстом:
class App { public function makeDatabase() { return new PDO('sqlite::memory:'); } } $app = new App(); $instantiator = function () { return $this->makeDatabase(); }; $boundInstantiator = $instantiator->bindTo($app, App::class); $db = $boundInstantiator(); // полноценный lazy-loading
DI-контейнер может хранить $boundInstantiator и вызывать его по мере надобности.
Нужно знать
Сериализация замыканий
В стандартном PHP замыкания не сериализуемы:
$fn = function () {}; serialize($fn); // Fatal error
С 7.4+ можно использовать сторонние решения, например, opis/closure, которые умеют сериализовывать даже привязанные замыкания. Но есть важный момент: bindTo() не сериализует scope, только объект $this. При восстановлении bindTo()-замыкание может потерять доступ к приватным членам, если не использовать специальный сериализатор.
Если важно сохранить контекст и scope — используйте Closure::bind() + OpisClosure::serialize().
Почему bindTo не может менять scope
Это ограничение встроено в движок PHP. Метод bindTo() специально блокирует смену области видимости по соображениям безопасности. Чтобы изменить scope, необходимо использовать Closure::bind(), потому что он создаёт полностью новую структуру, а bindTo() лишь привязывает $this к существующей:
$fn->bindTo($obj, 'AnotherClass'); // Fatal error Closure::bind($fn, $obj, 'AnotherClass'); // OK
ZVAL и VM
На уровне движка каждое замыкание — это ZVAL с типом IS_OBJECT, обёрнутый в zend_closure. Когда вы вызываете bind(), движок создаёт новый объект zend_closure, копируя handler, opcodes, статические переменные и — главное — новую this_ptr и called_scope.
Таким образом:
-
$this— этоthis_ptr; -
scope— этоcalled_scope.
Историческая справка
Возможность Closure::bind() появилась в PHP 5.4 — вместе с анонимными функциями как полноценными объектами.
До этого момента замыкания не были полноценными first-class citizen’ами: можно было передавать функцию как коллбэк, но нельзя было сделать вот это:
$fn = function () {}; $fn = $fn->bindTo($obj, 'Scope');
Тогда же появился класс Closure, возможность создавать замыкания с сохранением контекста и — как побочный эффект — доступ к приватному API через scope.
Когда использовать, а когда нет
Да, если:
-
вы пишете фреймворк или контейнер;
-
вам нужно обернуть поведение метода (логгирование, транзакции, AOP);
-
вы мокаете приватные зависимости в тестах;
-
вы делаете ленивую инициализацию зависимостей;
-
вам нужен доступ к внутренностям объекта без рефлексии.
Нет, если:
-
вы просто хотите сэкономить на геттере;
-
вы делаете это без нужды — привязка closure стоит ресурсов.
А вы использовали bind() в проде или тестах? Делитесь опытом в комментах.
Если вы хотите углубить свои знания в современных инструментах разработки и архитектуры приложений, предлагаю обратить внимание на открытые уроки в Otus, которые помогут улучшить навыки и познакомиться с новыми подходами в работе с PHP и мониторингом. Темы уроков:
ссылка на оригинал статьи https://habr.com/ru/articles/898094/
Добавить комментарий