laravel/serializable-closure
Securely serialize and unserialize PHP closures with Laravel’s fork of opis/closure 3.x, updated for modern PHP without requiring FFI. Wrap closures in SerializableClosure, set a secret key for signing, serialize safely, then restore with getClosure().
Installation:
composer require laravel/serializable-closure
Ensure your project uses PHP 7.4+ (tested up to PHP 8.5).
Basic Usage:
use Laravel\SerializableClosure\SerializableClosure;
// Set a secret key (required for security)
SerializableClosure::setSecretKey('your_secure_random_key_here');
// Wrap and serialize
$closure = fn () => 'Hello';
$serialized = serialize(new SerializableClosure($closure));
// Unserialize and retrieve
$unserialized = unserialize($serialized);
$originalClosure = $unserialized->getClosure();
echo $originalClosure(); // Output: "Hello"
First Use Case:
jobs table) to defer execution of logic until later.
$job = new Job(['closure' => serialize(new SerializableClosure(fn () => $user->sendWelcomeEmail())]);
jobs table).
$serialized = serialize(new SerializableClosure(fn () => $user->updateProfile()));
$job->closure = $serialized;
$job->save();
$job = Job::find($id);
$closure = unserialize($job->closure)->getClosure();
$closure(); // Execute deferred logic
Event::listen('user.created', function () {
$closure = fn ($user) => Log::info("User {$user->id} created");
$serialized = serialize(new SerializableClosure($closure));
// Store $serialized in DB/cache for later replay
});
$middleware = serialize(new SerializableClosure(fn ($request) => auth()->check()));
// Store in route middleware or cache
$stub = serialize(new SerializableClosure(fn () => 'test'));
$this->app->instance('closure_key', unserialize($stub));
Queue Jobs:
Use with Illuminate\Queue\Jobs\Job to defer complex logic.
class ComplexJob implements ShouldQueue {
public function handle() {
$closure = unserialize($this->closure)->getClosure();
$closure($this->payload);
}
}
Cached Closures: Store serialized closures in Redis/Memcached for shared execution.
Cache::put('dynamic_closure', serialize(new SerializableClosure($closure)), now()->addHour());
Policy/Authorization: Serialize authorization logic for dynamic role-based access.
$policy = serialize(new SerializableClosure(fn ($user, $model) => $user->can('edit', $model)));
openssl_random_pseudo_bytes(32)) for security.$unsigned = new SerializableClosure($closure, sign: false);
Closure Factories: Create reusable factories for common patterns.
class ClosureFactory {
public static function deferredEmail($userId) {
return serialize(new SerializableClosure(
fn () => User::find($userId)->sendEmail()
));
}
}
Versioned Closures: Include a version in the serialized payload to handle breaking changes.
$serialized = serialize([
'version' => '1.0',
'closure' => new SerializableClosure($closure),
]);
Closure Introspection: Use reflection to inspect serialized closures (e.g., for debugging).
$reflection = new ReflectionFunction($closure);
$params = $reflection->getParameters();
REPL Environments (Tinker/Artisan):
Multiple Closures on One Line:
// ❌ Problematic
$a = fn () => 1; $b = fn () => 2;
// ✅ Safe
$a = fn () => 1;
$b = fn () => 2;
Class Properties:
SerializableClosure as a class property may be unwrapped during deserialization.private properties and ensure proper type hints.
private $serializedClosure; // Store as string, not object
PHP 8.4+ Virtual Properties:
v2.0.11+ which includes fixes for this.Method-Only Attributes:
[Attribute] on methods.v2.0.11+ or avoid such attributes in closures.Carbon/DateTime Objects:
->toDateTimeString().Validation:
if (!SerializableClosure::getSecretKey()) {
throw new \RuntimeException('Secret key not set!');
}
Inspect Serialized Data:
var_dump() to check the serialized structure:
$serialized = serialize(new SerializableClosure($closure));
var_dump($serialized); // Look for errors or unexpected data
Handle Exceptions:
try {
$closure = unserialize($serialized)->getClosure();
} catch (\Throwable $e) {
Log::error("Closure deserialization failed: {$e->getMessage()}");
throw new \RuntimeException('Invalid closure data');
}
Test Edge Cases:
$this.fn ($arg1, $arg2 = 'default') => ...).Custom Serialization:
SerializableClosure to add metadata:
class CustomSerializableClosure extends SerializableClosure {
public function __construct(\Closure $closure, public string $description = '') {
parent::__construct($closure);
}
}
Alternative Storage:
json_encode() for JSON-compatible storage:
$serialized = json_encode([
'closure' => serialize(new SerializableClosure($closure)),
'metadata' => ['version' => '1.0'],
]);
Security Enhancements:
$hmac = hash_hmac('sha256', $serialized, $secretKey);
// Store both $serialized and $hmac
Performance Optimization:
$cache = new \Symfony\Component\Cache\Adapter\FilesystemAdapter();
$cache->set('closure_key', $serialized, new \DateInterval('P1D'));
Secret Key Management:
.env:
CLOSURE_SECRET_KEY=your_secure_key_here
bootstrap/app.php:
SerializableClosure::setSecretKey(config('app.closure_secret_key'));
Unsigned Closures:
$unsigned = new SerializableClosure($closure,
How can I help you explore Laravel packages today?