Как стать автором
Поиск
Написать публикацию
Обновить
521.83
OTUS
Развиваем технологии, обучая их создателей

Closure::bind() и bindTo() в PHP

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров1.7K

Привет, Хабр.

Сегодня рассмотрим, как в 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 и мониторингом. Темы уроков:

  • 8 апреля: Продвинутая архитектура приложений на PHP.
    Подробнее

  • 15 апреля: Организация мониторинга с помощью Grafana.
    Подробнее

Теги:
Хабы:
Всего голосов 15: ↑8 и ↓7+4
Комментарии1

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS