rcalicdan / serializer
code serializer for ipc and parallel execution libraries built on top of opis/closure
Fund package maintenance!
rcalicdan
Installs: 1
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/rcalicdan/serializer
Requires
- php: ^8.3
- opis/closure: ^4.0
Requires (Dev)
- laravel/pint: ^1.0
- pestphp/pest: ^4.0
- phpstan/phpstan: ^2.1
README
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.
Installation
composer require rcalicdan/serializer
Requirements
- PHP 8.3 or higher
- opis/closure library (automatically installed as dependency)
Overview
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.
Basic Usage
Static API (Recommended)
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); }
Non-Static API
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();
When to Use Non-Static API
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 } }
Supported Callback Types
The library automatically detects and handles these callback types:
1. String Functions
Native PHP functions passed as strings.
$callback = 'strtoupper'; $serialized = CallbackSerializer::serialize($callback); $callback = CallbackSerializer::unserialize($serialized); echo $callback('hello'); // Outputs: HELLO
Priority: 100 (highest)
2. Static Methods
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: 8
Priority: 90
3. Closures
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: 50
Priority: 80
4. Instance Methods
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
5. Anonymous Classes
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: 10
Priority: 70
6. Invokable Objects
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: 10
Priority: 60
Context Serialization
Serialize and unserialize context data (arrays) alongside callbacks, useful for passing state in parallel execution scenarios.
Static API
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); }
Non-Static API
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); }
Use Case: Parallel Execution
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 parent
Advanced Usage
Custom Serializers (Static API)
Create 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);
Custom Serializers (Non-Static API)
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);
Get Serializer Information
Static API
$info = CallbackSerializer::getSerializerInfo(); foreach ($info as $serializer) { echo "Class: {$serializer['class']}\n"; echo "Name: {$serializer['name']}\n"; echo "Priority: {$serializer['priority']}\n\n"; }
Non-Static API
$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
...
Reset Singleton (Testing)
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);
Exception Handling
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(); }
Architecture
Automatic Callback Type Detection
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.
Serializer Priority System
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
Singleton Pattern (Static API)
The static CallbackSerializer class uses a singleton pattern to maintain a single instance of CallbackSerializationManager, ensuring consistent serializer registration across your application.
Direct Instantiation (Non-Static API)
The CallbackSerializationManager can be instantiated directly for scenarios requiring multiple independent configurations or better testability.
Testing
Testing with Static API
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)); } }
Testing with Non-Static API
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)); } }
Limitations
- 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
License
MIT License
Contributing
Contributions are welcome! Please submit pull requests or open issues on GitHub.
Credits
This library uses opis/closure for closure serialization.