Laravel Serializable Closure: putting a PHP closure through serialize()
Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.
At a glance
- What is it?
- This package wraps a PHP closure in an object that PHP can serialize, signs it with a secret key, and hands the closure back on the other side. It exists as a Laravel fork of opis/closure 3.x because the upstream 4.x rewrite depends on FFI, which is not enabled by default in web requests.
- Who is it for?
- Laravel Serializable Closure is the right answer for a Laravel application that needs a closure to survive a round trip through storage, since wrapping the closure in the package object and calling setSecretKey is the whole integration, and the package is maintained inside the Laravel organisation on the 2.x branch with the last push on 2026-10-14.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A fork that exists because of FFI
The reason this package is a fork rather than a dependency is specific and worth understanding. The project is a fork of the opis/closure 3.x package, and the stated reason is that the upcoming upstream version 4.x is a complete rewrite built on top of the FFI extension. Laravel decided to fork because it is a web framework, and FFI is not enabled by default in web requests, so the 4.x line could not simply be adopted. The fork therefore keeps the 3.x series alive while adding support for new PHP versions. That decision has a lasting consequence: this package will track the 3.x behaviour rather than the 4.x rewrite, so anyone expecting the newer upstream design should look at what the fork actually contains rather than the upstream documentation.
A secret key is the recommended part of the recipe
The usage example is short enough to read in one breath, and the ordering in it matters. A closure is created, a secret key is set on the package, the closure is wrapped in a `SerializableClosure` object, that object is passed to PHP's own `serialize`, and on the way back `unserialize` returns a wrapper whose `getClosure` gives you the original callable again. The key step is marked as recommended in the documentation, and it is the one line that is easy to skip in a hurry. Without a key the payload is just an object graph, and with a key the package has something to verify the payload against, which is the difference between a string you can hand to a queue and a string you would trust blindly. The default branch is 2.x, and the package requires PHP 7.4 or newer.
Installing and running the first round trip
Installation is a single Composer command, and the project requires PHP 7.4 or newer:
composer require laravel/serializable-closureThe whole flow, from a closure to a string and back to a callable, looks like this:
use Laravel\SerializableClosure\SerializableClosure;
$closure = fn () => 'james';
// Recommended
SerializableClosure::setSecretKey('secret');
$serialized = serialize(new SerializableClosure($closure));
$closure = unserialize($serialized)->getClosure();
echo $closure(); // james;The class name carries the namespace `Laravel\SerializableClosure`, so a project already using another closure serialization library will need to change its imports rather than swap a single class name. Notice also that the secret key is set on the static class, not passed to the constructor, which means the key has to be configured in one place, in the same way for every producer and consumer of the payload. If two services serialize and unserialize the same closures, they have to share that key or the round trip will not validate.
The two caveats that cause real debugging sessions
The documentation lists two caveats, and both of them turn into confusing behaviour rather than clean errors. The first is that serializing closures on REPL environments like Laravel Tinker is not supported, so a quick check in the console will behave differently from the same code running under a web request, and the difference is not obvious from the failure. The second is that multiple closures defined on the same source line with identical signatures may not be indistinguishable after serialization; the documented advice is to place each closure on its own line to avoid this. Both point at the same underlying constraint, which is that the serialized form has to carry enough information to identify the code, and code that sits on one line with a shared signature does not leave much to identify it by.
What the 2.x branch looks like in practice
The release history shows a package that is still being worked on. The recent versions are v2.1.0 published on 2026-09-22, v2.0.16 on 2026-08-25, and v2.0.15 on 2026-07-21, and the last push to the repository was on 2026-10-14, so the branch is receiving changes across the 2.0.x and 2.1.x lines. The repository layout is a normal PHP package: `src/` and `tests/` directories, a `composer.json`, a `phpstan.neon` for static analysis and a `phpunit.xml.dist` for the test configuration, plus a `CHANGELOG.md` and an `UPGRADE.md`. That last file matters more than it sounds, because a package that carries security behaviour across minor versions needs a written upgrade path, and the project keeps one. The licence is MIT, and the project follows the Laravel contribution guide and code of conduct.
Where the package is the wrong choice
There are two clear cases. The first is a REPL workflow, where the package simply does not work, and the documentation says so before you find out. The second is a project that wants the features of the rewritten upstream line: this fork stays on the 3.x series on purpose, and the upstream 4.x work is a complete rewrite on FFI, so the two are not interchangeable and the documentation for one is not documentation for the other. A third, softer case is a codebase that stores closures but has no real need to: the package exists because a closure has to cross a boundary, whether that is a queue, a cache entry or a database column, and if the closure can be replaced by a named function or a class method reference, that is less machinery to secure. The documentation does not describe limits on what a closure may capture, so a closure that holds onto a database connection or a service container is worth thinking about before it is stored.
Editorial conclusion
Laravel Serializable Closure is the right answer for a Laravel application that needs a closure to survive a round trip through storage, since wrapping the closure in the package object and calling setSecretKey is the whole integration, and the package is maintained inside the Laravel organisation on the 2.x branch with the last push on 2026-10-14. It is the wrong tool for a REPL session, because serializing closures on environments like Laravel Tinker is not supported, and it is the wrong tool if you need the features of the rewritten upstream line, since this branch deliberately stays on the 3.x series. Before you depend on it, set a secret key rather than skipping the recommended call, keep each closure on its own source line so that two closures with identical signatures remain distinguishable, and read UPGRADE.md in the repository when moving between 2.x releases.
Frequently asked questions
How do I install Laravel Serializable Closure?
Install it with Composer using composer require laravel/serializable-closure. The package requires PHP 7.4 or newer.
How do I serialize a closure in PHP with this package?
Set a secret key with SerializableClosure::setSecretKey, wrap the closure in a new SerializableClosure object, pass that to PHP's serialize, and on the other side call unserialize and then getClosure to get the callable back. The documented example is in the package documentation.
Why did Laravel fork opis/closure?
The upcoming upstream version 4.x is a complete rewrite on top of the FFI extension, and FFI is not enabled by default in web requests, which matters for a web framework. The fork keeps the 3.x series available while adding support for new PHP versions.
Does Serializable Closure work in Laravel Tinker?
No. The documentation states that serializing closures on REPL environments like Laravel Tinker is not supported, so a check in the console will not behave like the same code in a web request.
Why can two of my closures become indistinguishable after serialization?
Multiple closures defined on the same source line with identical signatures may not be distinguishable once serialized. The documented workaround is to place each closure on its own line.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/laravel-serializable-closure)Was this useful?