An open API service indexing awesome lists of open source software.

https://github.com/rcalicdan/serializer

Serializer utility tool for serializing callable for IPC and parallel execution system built on top of opis/closure
https://github.com/rcalicdan/serializer

ipc serialization-library

Last synced: 5 months ago
JSON representation

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

Awesome Lists containing this project

README

          

# 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

```bash
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:

```php
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:

```php
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

```php
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.

```php
$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']`.

```php
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.

```php
$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']`.

```php
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.

```php
$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.

```php
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

```php
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

```php
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:

```php
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`.

```php
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)

```php
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

```php
$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

```php
$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.

```php
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.

```php
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

```php
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

```php
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](https://github.com/opis/closure) for closure serialization.