PHP 8.5 let you put closures inside constant expressions, like attribute arguments and parameter default values, but serialize() still refuses every closure. This RFC from Nicolas Grekas makes those closures serializable by storing a reference to where the closure is declared instead of the closure's code.
Why it matters
Tools like Doctrine and the Symfony Validator read a class's attributes once and cache the result with serialize(). If an attribute holds a closure, that caching step fails:
class Order { #[Assert\Callback(static function (string $value, ExecutionContextInterface $context) { // ...custom validation... })] public string $billingAddress; }
The cache layer catches the error and rebuilds the metadata on every request. Nothing breaks, but your app gets slower. Frameworks work around this today with their own wrapper objects, and this RFC fixes it once in the engine.
How it works
$closure = (new ReflectionProperty(Order::class, 'billingAddress')) ->getAttributes()[0]->getArguments()[0]; unserialize(serialize($closure))(); // behaves exactly like $closure()
The serialized value names the class, the member that holds the closure, and the closure's position within it. When you unserialize, PHP loads the class and rebuilds the closure as if the attribute had just been read.
- Anonymous closures like
static function () {}are located by position. A hash of the code confirms the position still points at the same closure, so if you reorder two attributes, the hash won't match andunserialize()throws. - First-class callables like
self::isStrict(...)are located by name.
Only closures declared in attribute arguments and parameter defaults qualify. Closures that capture variables, bind $this, or get created at runtime stay unserializable, with the same error as today.
A payload can only point to a closure a class already declares, which keeps a forged payload from calling something like system. If you pass allowed_classes to unserialize(), you'll need to list both Closure and the declaring class.
What it means for existing code
Code that doesn't serialize closures sees no change. Three things do change:
serialize()works on these closures, where it used to throw.unserialize()acceptsClosurepayloads.method_exists($closure, '__serialize')returnstrue.
Where it stands
The RFC is under discussion on the internals mailing list. It's at version 0.3, dated June 10, 2026, and targets PHP 8.6. Vote dates haven't been set yet, and it needs a two-thirds majority to pass.