Skip to content

Serialize closures declared in constant expressions#22716

Open
nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:serializable-closures
Open

Serialize closures declared in constant expressions#22716
nicolas-grekas wants to merge 1 commit into
php:masterfrom
nicolas-grekas:serializable-closures

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

RFC: https://wiki.php.net/rfc/serializable_closures

Closures declared in constant expressions of a class member (anonymous closures and first-class callables, of any visibility) become serializable as references to their declaration site. unserialize() resolves the reference against the loaded class; payloads contain no code and can only designate closures that classes declare. Runtime-created closures keep refusing to serialize with the current error.

The public surface is serialize()/unserialize() alone, through the regular __serialize()/__unserialize() protocol; no new reflection API. Exporters recreate a closure from generated code by embedding the payload in an unserialize('O:7:"Closure":...') call, which goes through the same resolution and verification.

The __serialize() payload is [<object properties>, [<tag>, <reference>]] (the shape ext/uri and ext/random use), where the tag names the reference kind; only "const-expr" exists today. The reference is four fields, [class, site, key, hash]. site names the class member that declares the closure (a constant name, $prop, $prop::get()/$prop::set(), or method(), with parameters folded into their method; empty for an attribute on the class itself), so a reference stays valid when unrelated members of the class change. For an anonymous closure key is its integer rank among the element's closures; for a first-class callable it is the callable's name ("Called::method" / "function"), since the name is already the address.

hash guards an anonymous rank: a hash of the closure's pretty-printed code, computed at compile time and stored on the declaration's AST node (persisted by opcache), so a rank that comes to name a different closure (e.g. after two attributes on the same member are swapped) fails loudly instead of resolving silently. A producer that cannot compute it (a userland polyfill) passes 0 and the rank resolves positionally, unverified. First-class callables carry 0 too: a name cannot drift.

Comment thread ext/reflection/php_reflection.c Outdated
Comment thread ext/reflection/php_reflection.c Outdated
@nicolas-grekas
nicolas-grekas force-pushed the serializable-closures branch 2 times, most recently from 814db1c to fae8b5c Compare July 14, 2026 15:14
Comment thread Zend/zend_closures.c Outdated
Comment thread Zend/zend_compile.c Outdated
Comment thread Zend/zend_closures.c Outdated
Comment thread Zend/zend_closures.c
Comment thread Zend/zend_closures.c Outdated
Comment thread Zend/zend_closures.c
Comment thread Zend/zend_closures.c
Comment thread Zend/zend_closures.c Outdated
Comment thread Zend/zend_closures.c
Comment thread Zend/zend_compile.h Outdated
@nicolas-grekas
nicolas-grekas force-pushed the serializable-closures branch from fae8b5c to 04c14b1 Compare July 14, 2026 16:08
@nicolas-grekas
nicolas-grekas requested a review from bukka as a code owner July 14, 2026 16:08
@nicolas-grekas
nicolas-grekas force-pushed the serializable-closures branch 7 times, most recently from fffacae to 402dfd2 Compare July 18, 2026 09:59
@nicolas-grekas
nicolas-grekas force-pushed the serializable-closures branch from 402dfd2 to 732604f Compare July 24, 2026 14:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants