A PHP library for serializing and unserializing various types of callbacks, including closures, static methods, instance methods, and more. This library is primarily designed for parallel execution and inter-process communication (IPC) serialization, automatically detecting callback types and using the appropriate serialization strategy.
composer require rcalicdan/serializer- PHP 8.3 or higher
- opis/closure library (automatically installed as dependency)
The Callback Serializer library provides a robust solution for serializing PHP callbacks, particularly useful for:
- Parallel execution libraries that need to pass callbacks between processes
- Inter-process communication (IPC) where callbacks must be serialized and transmitted
- Queue systems that need to serialize job callbacks
- Event systems that persist event listeners
- Workflow engines that save state
The library automatically detects the callback type and uses the appropriate serialization strategy, removing the need for manual callback type checking.
The static CallbackSerializer class provides a convenient singleton-based API:
use Rcalicdan\Serializer\CallbackSerializer;
// Serialize a closure
$closure = function ($x) {
return $x * 2;
};
$serialized = CallbackSerializer::serialize($closure);
// Unserialize and execute
$callback = CallbackSerializer::unserialize($serialized);
$result = $callback(5); // Returns: 10
// Check if callback can be serialized
if (CallbackSerializer::canSerialize($closure)) {
$serialized = CallbackSerializer::serialize($closure);
}For applications requiring multiple independent serializer instances or more control, use the CallbackSerializationManager directly:
use Rcalicdan\Serializer\CallbackSerializationManager;
// Create a manager instance
$manager = new CallbackSerializationManager();
// Serialize a callback
$closure = function ($x) {
return $x * 2;
};
$serialized = $manager->serializeCallback($closure);
// Unserialize a callback
$callback = $manager->unserializeCallback($serialized);
$result = $callback(5); // Returns: 10
// Check if callback can be serialized
if ($manager->canSerializeCallback($closure)) {
$serialized = $manager->serializeCallback($closure);
}
// Get serializer information
$info = $manager->getSerializerInfo();Use CallbackSerializationManager directly when you need:
- Multiple independent serializer configurations in the same application
- Different custom serializers for different contexts
- More explicit dependency injection
- Easier unit testing without singleton state
use Rcalicdan\Serializer\CallbackSerializationManager;
class ParallelExecutor
{
private CallbackSerializationManager $serializer;
public function __construct(?CallbackSerializationManager $serializer = null)
{
$this->serializer = $serializer ?? new CallbackSerializationManager();
}
public function executeInParallel(callable $callback, array $data): void
{
$serialized = $this->serializer->serializeCallback($callback);
// Send to child process via IPC
$this->sendToChildProcess($serialized, $data);
}
private function sendToChildProcess(string $serialized, array $data): void
{
// IPC implementation
}
}The library automatically detects and handles these callback types:
Native PHP functions passed as strings.
$callback = 'strtoupper';
$serialized = CallbackSerializer::serialize($callback);
$callback = CallbackSerializer::unserialize($serialized);
echo $callback('hello'); // Outputs: HELLOPriority: 100 (highest)
Class static methods as array [ClassName::class, 'methodName'].
class Calculator
{
public static function add($a, $b)
{
return $a + $b;
}
}
$callback = [Calculator::class, 'add'];
$serialized = CallbackSerializer::serialize($callback);
$callback = CallbackSerializer::unserialize($serialized);
echo $callback(5, 3); // Outputs: 8Priority: 90
Anonymous functions with variable binding support.
$multiplier = 10;
$closure = function ($x) use ($multiplier) {
return $x * $multiplier;
};
$serialized = CallbackSerializer::serialize($closure);
$callback = CallbackSerializer::unserialize($serialized);
echo $callback(5); // Outputs: 50Priority: 80
Object instance methods as array [$object, 'methodName'].
class Greeter
{
private $greeting = 'Hello';
public function greet($name)
{
return "{$this->greeting}, {$name}!";
}
}
$greeter = new Greeter();
$callback = [$greeter, 'greet'];
$serialized = CallbackSerializer::serialize($callback);
$callback = CallbackSerializer::unserialize($serialized);
echo $callback('World'); // Outputs: Hello, World!Priority: 70
Anonymous class instances with __invoke method.
$callback = new class {
public function __invoke($x)
{
return $x * 2;
}
};
$serialized = CallbackSerializer::serialize($callback);
$callback = CallbackSerializer::unserialize($serialized);
echo $callback(5); // Outputs: 10Priority: 70
Regular class instances with __invoke method.
class Doubler
{
public function __invoke($x)
{
return $x * 2;
}
}
$callback = new Doubler();
$serialized = CallbackSerializer::serialize($callback);
$callback = CallbackSerializer::unserialize($serialized);
echo $callback(5); // Outputs: 10Priority: 60
Serialize and unserialize context data (arrays) alongside callbacks, useful for passing state in parallel execution scenarios.
use Rcalicdan\Serializer\CallbackSerializer;
$context = [
'user_id' => 123,
'settings' => ['theme' => 'dark'],
'callback' => function ($x) {
return $x * 2;
},
];
// Serialize context
$serialized = CallbackSerializer::serializeContext($context);
// Unserialize context
$restored = CallbackSerializer::unserializeContext($serialized);
// Context is fully restored including nested callbacks
echo $restored['callback'](5); // Outputs: 10
// Check if context can be serialized
if (CallbackSerializer::canSerializeContext($context)) {
$serialized = CallbackSerializer::serializeContext($context);
}use Rcalicdan\Serializer\CallbackSerializationManager;
$manager = new CallbackSerializationManager();
$context = [
'user_id' => 123,
'settings' => ['theme' => 'dark'],
];
// Serialize context
$serialized = $manager->serializeContext($context);
// Unserialize context
$restored = $manager->unserializeContext($serialized);
// Check if context can be serialized
if ($manager->canSerializeContext($context)) {
$serialized = $manager->serializeContext($context);
}Example of using the library for parallel execution with IPC:
use Rcalicdan\Serializer\CallbackSerializer;
// Parent process
$task = function ($data) {
// Heavy computation
return array_sum($data) * 2;
};
$serializedTask = CallbackSerializer::serialize($task);
$serializedContext = CallbackSerializer::serializeContext(['data' => [1, 2, 3, 4, 5]]);
// Send to child process via socket/pipe/shared memory
socket_write($socket, $serializedTask);
socket_write($socket, $serializedContext);
// Child process
$serializedTask = socket_read($socket, 8192);
$serializedContext = socket_read($socket, 8192);
$task = CallbackSerializer::unserialize($serializedTask);
$context = CallbackSerializer::unserializeContext($serializedContext);
$result = $task($context['data']);
// Send result back to parentCreate custom serializers for specific callback types by implementing the CallbackSerializerInterface.
use Rcalicdan\Serializer\Interfaces\CallbackSerializerInterface;
use Rcalicdan\Serializer\Exceptions\SerializationException;
use Rcalicdan\Serializer\CallbackSerializer;
class CustomSerializer implements CallbackSerializerInterface
{
public function canSerialize(mixed $callback): bool
{
// Check if this serializer can handle the callback
return $callback instanceof MyCustomCallable;
}
public function serialize(mixed $callback): string
{
// Serialize the callback
if (!$this->canSerialize($callback)) {
throw new SerializationException('Cannot serialize this callback');
}
return json_encode(['custom' => $callback->getData()]);
}
public function unserialize(string $serialized): mixed
{
// Unserialize the callback
$data = json_decode($serialized, true);
return new MyCustomCallable($data['custom']);
}
public function getPriority(): int
{
// Higher priority = checked first (100 = highest)
return 95;
}
}
// Register custom serializer
$customSerializer = new CustomSerializer();
CallbackSerializer::addSerializer($customSerializer);
// Now your custom serializer will be used automatically
$callback = new MyCustomCallable();
$serialized = CallbackSerializer::serialize($callback);use Rcalicdan\Serializer\CallbackSerializationManager;
$manager = new CallbackSerializationManager();
// Add custom serializer
$customSerializer = new CustomSerializer();
$manager->addSerializer($customSerializer);
// Use the manager with custom serializer
$callback = new MyCustomCallable();
$serialized = $manager->serializeCallback($callback);$info = CallbackSerializer::getSerializerInfo();
foreach ($info as $serializer) {
echo "Class: {$serializer['class']}\n";
echo "Name: {$serializer['name']}\n";
echo "Priority: {$serializer['priority']}\n\n";
}$manager = new CallbackSerializationManager();
$info = $manager->getSerializerInfo();
foreach ($info as $serializer) {
echo "Class: {$serializer['class']}\n";
echo "Name: {$serializer['name']}\n";
echo "Priority: {$serializer['priority']}\n\n";
}Output:
Class: Rcalicdan\Serializer\Serializers\StringFunctionSerializer
Name: StringFunctionSerializer
Priority: 100
Class: Rcalicdan\Serializer\Serializers\StaticMethodSerializer
Name: StaticMethodSerializer
Priority: 90
Class: Rcalicdan\Serializer\Serializers\ClosureSerializer
Name: ClosureSerializer
Priority: 80
...
Useful for resetting the serializer state in unit tests when using the static API.
use Rcalicdan\Serializer\CallbackSerializer;
// Reset the singleton instance
CallbackSerializer::reset();
// Fresh instance with default serializers
$callback = function () {};
$serialized = CallbackSerializer::serialize($callback);The library throws SerializationException when serialization or unserialization fails.
use Rcalicdan\Serializer\CallbackSerializer;
use Rcalicdan\Serializer\Exceptions\SerializationException;
try {
$callback = /* some callback */;
$serialized = CallbackSerializer::serialize($callback);
} catch (SerializationException $e) {
echo "Serialization failed: " . $e->getMessage();
}
try {
$callback = CallbackSerializer::unserialize($serialized);
} catch (SerializationException $e) {
echo "Unserialization failed: " . $e->getMessage();
}The library uses a priority-based system to automatically detect and handle different callback types. When you call serialize(), the library:
- Iterates through registered serializers in priority order (highest first)
- Checks if each serializer can handle the callback using
canSerialize() - Uses the first matching serializer to perform the serialization
- Throws
SerializationExceptionif no serializer matches
This automatic detection means you never need to manually determine the callback type.
Serializers are checked in order of priority (highest first):
- StringFunctionSerializer (100) - Fastest, no complex serialization needed
- StaticMethodSerializer (90) - Simple string serialization
- ClosureSerializer (80) - Uses opis/closure for complex serialization
- InstanceMethodSerializer (70) - Serializes object instances
- AnonymousClassSerializer (70) - Handles anonymous classes
- InvokableObjectSerializer (60) - Catches remaining invokable objects
The static CallbackSerializer class uses a singleton pattern to maintain a single instance of CallbackSerializationManager, ensuring consistent serializer registration across your application.
The CallbackSerializationManager can be instantiated directly for scenarios requiring multiple independent configurations or better testability.
use PHPUnit\Framework\TestCase;
use Rcalicdan\Serializer\CallbackSerializer;
class CallbackSerializerTest extends TestCase
{
protected function setUp(): void
{
// Reset singleton between tests
CallbackSerializer::reset();
}
public function test_can_serialize_closure(): void
{
$closure = function ($x) {
return $x * 2;
};
$this->assertTrue(CallbackSerializer::canSerialize($closure));
$serialized = CallbackSerializer::serialize($closure);
$unserialized = CallbackSerializer::unserialize($serialized);
$this->assertEquals(10, $unserialized(5));
}
}use PHPUnit\Framework\TestCase;
use Rcalicdan\Serializer\CallbackSerializationManager;
class CallbackSerializationManagerTest extends TestCase
{
private CallbackSerializationManager $manager;
protected function setUp(): void
{
// Create fresh instance for each test
$this->manager = new CallbackSerializationManager();
}
public function test_can_serialize_closure(): void
{
$closure = function ($x) {
return $x * 2;
};
$this->assertTrue($this->manager->canSerializeCallback($closure));
$serialized = $this->manager->serializeCallback($closure);
$unserialized = $this->manager->unserializeCallback($serialized);
$this->assertEquals(10, $unserialized(5));
}
}- Closures with resources (file handles, database connections) cannot be serialized
- Some objects with internal state may not serialize correctly
- Callbacks referencing unavailable classes will fail on unserialization
- Serialized callbacks may break if the referenced code changes between serialization and unserialization
MIT License
Contributions are welcome! Please submit pull requests or open issues on GitHub.
This library uses opis/closure for closure serialization.