PHP functions, closures, and callables
Status: Complete. Last reviewed 2026-08-28.
Functions are call boundaries: PHP evaluates arguments, binds them to parameters, executes in a local scope, and checks a declared return. Closures add captured lexical state; callables describe invokable targets; generators suspend and resume execution. These mechanisms overlap, but they have different identity, lifetime, typing, and compatibility consequences.
Parameters, values, and references
Section titled “Parameters, values, and references”Arguments are passed by value unless a parameter is declared by reference with &. “By value” means the parameter receives the value according to PHP’s value model: scalars are copied values, arrays can share storage until mutation, and object values are handles to instances. A function can therefore mutate a received object without a reference parameter, but assigning a new object to the parameter does not rebind the caller’s variable.
A by-reference parameter aliases the caller’s variable container. It should be reserved for an API whose explicit result is mutation of that variable, such as a low-level parser that advances an offset. References make data flow and static reasoning harder; returning a result object is usually clearer for application code.
Default argument values are evaluated as constant expressions and should express stable API behavior. Variadic parameters collect remaining positional arguments into an array. Argument unpacking expands arrays or traversables, with string keys acting as named arguments under modern PHP rules. Duplicate or invalid parameter names fail rather than being silently ignored.
Named arguments are part of the public contract
Section titled “Named arguments are part of the public contract”Named arguments allow callers to omit optional parameters and bind by parameter name instead of position. That improves readability for stable application-owned APIs, but it turns parameter names into compatibility surface. Renaming a public parameter can break callers even when its type and position remain unchanged.
This is particularly risky when calling vendor or extension functions by name: a library may not promise parameter-name stability across versions. For constructors with many independent options, a configuration object or named constructor often evolves more safely than a long optional parameter list. Do not use named arguments to conceal a function doing too many unrelated things.
When forwarding ...$arguments, remember that associative keys become names. A wrapper that once forwarded a numeric list may begin failing when input acquires a string key, or it may bind different parameters after a downstream rename. Validate forwarding maps at the wrapper boundary.
Closures capture deliberately
Section titled “Closures capture deliberately”An anonymous function is a Closure object. Its use list captures selected outer variables by value or by reference. By-value capture takes the current value when the closure is created; it does not repeatedly read the outer variable. For object values, the captured value is still an object handle, so later mutation of that instance remains visible.
By-reference capture aliases the outer variable, allowing later rebinding and mutation to be observed. That can implement an accumulator, but it creates temporal coupling and can retain more state than expected in a long-lived worker.
Arrow functions capture referenced outer variables automatically by value. Their single expression is returned implicitly. They cannot request reference capture through a use list, so code that needs evolving captured state should use an explicit closure—or preferably make the state an intentional object.
Non-static closures created in object context are normally bound to $this and class scope. Declaring a closure static prevents $this binding and can avoid accidentally retaining a large owning object. bindTo() and Closure::call() can change object/scope access, but that power should be limited to infrastructure such as carefully reviewed proxy or testing code.
Callable forms and normalization
Section titled “Callable forms and normalization”The callable pseudo-type accepts functions, closures, public method pairs, invokable objects, and other documented callable forms. Callable validity can depend on visibility and scope, which is why callable cannot be a property type. A Closure is an object with stable invocation independent of a future caller’s scope.
First-class callable syntax such as $service->handle(...) produces a Closure while respecting visibility at the creation point. Closure::fromCallable() provides similar normalization. Prefer these over string and array callables when storing or transporting behavior inside the application: they are easier for refactoring tools and static analyzers to follow, although serialization of executable behavior should still be avoided.
An invokable object gives behavior a named type, constructor dependencies, and testable state. It is a good choice when a callback represents a durable policy rather than one local expression.
Generators are resumable iterators
Section titled “Generators are resumable iterators”A function containing yield returns a Generator when called; its body begins when iteration advances it. Each yield suspends the frame, preserving local state until the next advance. This enables bounded-memory pipelines because the producer can generate one value at a time instead of constructing a complete array.
A generator does not reduce total computation or automatically make I/O asynchronous. It may move exceptions from function call time to iteration time, and it retains its suspended locals and referenced resources. Holding a generator without exhausting or closing it can therefore retain file handles, database cursors, or large objects.
yield from delegates iteration and can receive a delegated generator’s return value. Callers should generally depend on iterable or Iterator behavior rather than generator-specific methods unless two-way communication with send() or explicit return extraction is truly part of the protocol.
Generator pipelines need clear ownership of resources and transaction boundaries. Streaming database rows while performing slow network calls can hold a cursor or transaction far longer than expected. Page/keyset through durable data or copy required fields out before slow work when the consistency and resource trade-off demands it.
Failure modes and decision triggers
Section titled “Failure modes and decision triggers”- A closure captures a tenant or request object and is stored in a singleton, leaking request state in a worker.
- A wrapper forwards associative unpacked arguments and breaks after a vendor parameter rename.
- A by-reference accumulator makes results depend on prior callbacks and retry order.
- A generator throws halfway through consumption, after the caller has already emitted partial output.
- A lazy generator holds a transaction open while consumers perform unrelated work.
- A string callable survives static checks but fails under visibility or refactoring changes.
Choose a plain function for stateless local transformation, a closure for local behavior with small explicit capture, an invokable object for named behavior with dependencies, and a generator when incremental consumption materially changes memory or latency. Measure the actual retained state rather than assuming “lazy” means cheap.
Current and legacy context
Section titled “Current and legacy context”- Current: First-class callables, named arguments, arrow functions, fibers, and modern generator APIs are available on the PHP 8.5 baseline. PHP 8.5 also provides
Closure::getCurrent()for some recursive-closure use cases. - Common: String/array callables, explicit anonymous functions, and collection pipelines remain widespread across PHP 8.2–8.4 code.
- Legacy: APIs with output reference parameters, scope-dependent pseudo-callables, and callback strings assembled dynamically deserve migration tests. Preserve behavior before replacing their calling convention.
Interview practice
Section titled “Interview practice”- PHP-CALLABLES-01 — Explain capture by value and reference
- PHP-CALLABLES-02 — Choose a callable representation
- PHP-CALLABLES-03 — Treat named arguments as compatibility surface
- PHP-CALLABLES-04 — Explain object mutation without reference parameters
- PHP-CALLABLES-05 — Design a safe generator pipeline
- PHP-CALLABLES-06 — Diagnose captured state in a worker