In PHP, when does a closure bind $this automatically, and why would you make event-listener closures static in a long-running worker?
answer
- created inside a non-static method
- the closure holds the object alive
- static function or static fn
- Using $this when not in object context
- cannot bind an instance to a static closure
basics
~20 sA closure created inside a non-static method binds $this automatically, keeping that object alive as long as the closure lives. A static closure binds no object: it holds only what it captures and refuses later binding to an instance.
solid answer
~50 sAny `function () {}` or `fn () =>` created inside a non-static method **binds `$this` and the class scope automatically**, whether or not the body uses `$this`. The closure object then holds a reference to that object. Registered as a listener in a dispatcher that lives for the whole process, as in a queue worker handling conference registrations, it keeps the service and everything it references alive, and can form a reference cycle when the service also holds the dispatcher. Declaring it `static function` or `static fn` stops the binding: the closure holds only what its `use` list or body captures. Reading `$this` inside then throws `Error: Using $this when not in object context`, and `bindTo($obj)` on it returns `null` with the warning `Cannot bind an instance to a static closure`. So: make closures static unless they need the object, and capture specific collaborators instead of `$this`.
code
php · 23 lines<?php
declare(strict_types=1);
final class RegistrationService
{
/** @var list<Closure> */
public array $listeners = [];
public function register(): void
{
$this->listeners[] = function (string $email): void {}; // binds $this
$this->listeners[] = static function (string $email): void {}; // binds nothing
}
}
$service = new RegistrationService();
$service->register();
var_dump((new ReflectionFunction($service->listeners[0]))->getClosureThis() === $service); // true
var_dump((new ReflectionFunction($service->listeners[1]))->getClosureThis()); // NULL
$rebound = $service->listeners[1]->bindTo($service); // Warning: Cannot bind an instance to a static closure ...
var_dump($rebound); // NULLgo deeper
Recall that closures created inside methods can use $this automatically and that the static keyword turns that off.
Explain that the binding happens at creation regardless of the body, and what static closures forbid: $this access and later binding to an object.
Diagnose memory growth in a worker from listeners that bind short-lived services, and fix it with static closures that capture only needed collaborators.
Decide lifecycle rules for process-wide dispatchers and containers in long-running PHP, including when listeners must be deregistered and who owns them.
## Automatic `$this` binding When PHP evaluates a `function () {}` or `fn () =>` expression **inside a non-static method**, the resulting `Closure` object gets two things for free: - **`$this`**: the object the method was called on; - **the class scope**: the right to access that class's `private` and `protected` members. This happens whether or not the body mentions `$this`. The closure object keeps a reference to the object for as long as the closure itself exists. In a static method there is no object, so nothing is bound, although the class scope still is. ## Why that matters for listeners In a classic web request, every object is released when the request ends, so an extra reference held by a closure rarely matters. It matters in **long-running processes**: queue workers, schedulers, CLI daemons, application servers that keep the app in memory between requests. Consider a conference-registration workflow in which a `RegistrationService` registers listeners on a dispatcher that lives for the whole process: ```php $this->dispatcher->listen('attendee.registered', function (AttendeeRegistered $e) { $this->mailer->sendConfirmation($e->email); }); ``` The closure binds `$this`, the service, so: 1. **The service cannot be released** while the dispatcher holds the listener, together with everything the service references, such as a loaded seat map or a cached price list. 2. **A reference cycle forms** when the service also holds the dispatcher (`$this->dispatcher`). Reference counting alone cannot free a cycle; it waits for PHP's cycle collector, so memory is held longer. 3. **Re-registration multiplies it.** A worker that builds a fresh service per job and registers listeners each time accumulates closures, each holding a whole service, and memory grows job after job. ## Static closures Prefixing the closure with `static`, `static function (...) use (...) {}` or `static fn (...) =>`, turns off the automatic binding: - no `$this` is bound, so the closure holds only the variables it captures; - using `$this` inside throws `Error: Using $this when not in object context` when the line runs; - the closure **cannot be given an object later**: `bindTo($obj)` and `Closure::bind($c, $obj)` emit `Warning: Cannot bind an instance to a static closure, this will be an error in PHP 9` and return `null`, and PHP 8.5 formally deprecated doing so. The listener above becomes: ```php $mailer = $this->mailer; $this->dispatcher->listen('attendee.registered', static function (AttendeeRegistered $e) use ($mailer) { $mailer->sendConfirmation($e->email); }); ``` It now holds only the mailer, the one collaborator it needs. ## Arrow functions behave the same way `fn` arrow functions also bind `$this` automatically in a method, even though they capture outer variables by value. `static fn ($e) => $mailer->send($e)` is the equivalent opt-out, and the pattern of copying a property into a local variable first applies too. ## A diagnosis checklist When a long-running worker's memory climbs job after job and listeners are involved: - look for closures registered inside methods on objects that are recreated per job; - check whether the dispatcher or container is process-wide while the registering object was meant to be short-lived; - make listeners `static` and capture the specific services they call; - deregister listeners when the owning object is done, if the dispatcher supports it; - measure `memory_get_usage()` between jobs before and after the change rather than assuming the fix. ## Trade-offs | Choice | Holds | Can use `$this` | Can be rebound to an object | |---|---|---|---| | closure in a method | the object and captured variables | yes | yes, with `bindTo()` or `call()` | | `static` closure | captured variables only | no | no | | closure in a static method | captured variables only (no object exists) | no | yes, it is not declared static | ## When a listener genuinely needs the object Sometimes the callback must read the service's own state, such as a registration counter kept on the service. Then the binding is legitimate, and the fix is about **lifetime**, not syntax: make sure the service lives exactly as long as the dispatcher that holds its listener (for example, one process-wide instance), or deregister the listener when the service is discarded. What should not happen is a per-job object silently promoted to process lifetime by a closure. Making every closure `static` by default is a cheap habit: PHP then tells you, with an `Error` the first time such a closure touches `$this`, exactly which closures really needed the object.
- In PHP, does a closure created in a method bind $this even if its body never uses $this?Yes. The binding happens when the closure object is created inside a non-static method, regardless of the body, and `ReflectionFunction::getClosureThis()` on it returns the object. That is why an innocuous-looking listener can keep a large service alive. Declaring it `static` is the only way to prevent the binding at creation.
- Why can't the reference cycle between a service, its dispatcher and a bound listener be freed by reference counting alone?Each object in the cycle still has a reference count above zero because the next one points to it, even after the program drops its own variables. Within a running process PHP frees such garbage only when its cycle collector runs, triggered automatically once enough possible roots accumulate or explicitly by `gc_collect_cycles()`. Static listeners that capture only the mailer avoid creating the cycle.
saying these in an interview costs you the question
- A closure binds $this only if its body actually uses $this.
- Arrow functions never bind $this because they capture by value.
- A static closure can still be bound to an object later with bindTo().
- Closures in a listener list cannot keep objects alive once the request method returns.
- Using $this inside a static closure just evaluates to null.