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.

Adds Closure::fromConstExpr() and ReflectionFunction::getConstExprId()/getConstExprClass() for exporters.

The reference is an opaque "@" string naming the declaring element (class, constant, property, property hook, or method, with parameters folded into their method) and a key within it, so it stays valid when unrelated elements of the class change. For an anonymous closure the key is its 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.

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. An anonymous-closure id embeds a hash of the closure's pretty-printed code ("$billingAddress@0#4a3f"), computed at compile time and stored in spare bits of the declaration's AST node, 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. Both unserialize() and Closure::fromConstExpr() verify it, so exporter-generated cache files get the same protection as serialized payloads. Producers that cannot compute the hash (userland polyfills) may omit the "#" suffix; such references resolve positionally, unverified. First-class callable ids carry no hash: 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
uint8_t found_kind;
zend_string *found_name;
uint8_t found_hook;
} zend_constexpr_closure_walk;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct could use a union to show which fields are relevant for FCCs vs anonymous classes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on a closer look, not sure it's worth it

Comment thread Zend/zend_closures.c Outdated
Comment thread Zend/zend_closures.c Outdated
Comment thread Zend/zend_closures.c
ZEND_PARSE_PARAMETERS_NONE();

if (zend_constexpr_closure_ref(Z_OBJ_P(ZEND_THIS), &ce, &id) == FAILURE) {
zend_throw_exception(NULL, "Serialization of 'Closure' is not allowed", 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid confusions, we could specify that this is allowed in some cases

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a comment about this: I don't think changing the userland message is worth it

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 5 times, most recently from 74ac4be to 1590605 Compare July 14, 2026 17:08
@nicolas-grekas nicolas-grekas force-pushed the serializable-closures branch from 1590605 to fffacae Compare July 14, 2026 17:42
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