Skip to content

Serializer utility tool for serializing callable for IPC and parallel execution system built on top of opis/closure

License

Notifications You must be signed in to change notification settings

rcalicdan/serializer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Callback Serializer

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:

  1. Iterates through registered serializers in priority order (highest first)
  2. Checks if each serializer can handle the callback using canSerialize()
  3. Uses the first matching serializer to perform the serialization
  4. Throws SerializationException if 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):

  1. StringFunctionSerializer (100) - Fastest, no complex serialization needed
  2. StaticMethodSerializer (90) - Simple string serialization
  3. ClosureSerializer (80) - Uses opis/closure for complex serialization
  4. InstanceMethodSerializer (70) - Serializes object instances
  5. AnonymousClassSerializer (70) - Handles anonymous classes
  6. 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.

About

Serializer utility tool for serializing callable for IPC and parallel execution system built on top of opis/closure

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Languages