Object serialization
Serialization of arbitrary objects
Adding custom serializers
If you try to add custom serializers to your own classes, the recommended approach is to implement the standard
__serialize() / __unserialize()
magic methods. Treat closures and other objects just like they are serializable, we’ll do the rest.
In the next example you can safely omit __serialize
and __unserialize
methods, the code will still work.
However, we recommend implementing them as a best practice.
use function Opis\Closure\{serialize, unserialize};
class MyClass {
private Closure $callback;
public function __construct(Closure $fn) {
$this->callback = $fn;
}
public function test() {
$fn = $this->callback;
return $fn();
}
public function __serialize() : array {
return ["callback" => $this->callback];
}
public function __unserialize(array $data) : void {
$this->callback = $data["callback"];
}
}
$serialized = serialize(new MyClass(fn() => "it works"));
$object = unserialize($serialized);
echo $object->test(); // it works
We often have to deal with code that we cannot control because it is from a third party.
Don’t worry, you can add custom serializers for any class, or you can overwrite existing serializers,
by using the register()
function. The function receives as parameters the class name,
the serialization function and the deserialization function.
Opis Closure already provides a generic serialization method for all objects, you are not required to implement one for every class. Here’s the order we use:
- registered custom serializer using
register()
- magic methods inside class
__serialize() / __unserialize()
- fallback to a builtin generic object serialization / deserialization
use Some\Vendor\ExternalClass;
use function Opis\Closure\register;
register(
ExternalClass::class,
static function (ExternalClass $object): array {
// we must always return an array from a serializer
return [
"a" => $object->getA(),
"b" => $object->getB(),
];
},
static function (array &$data, callable $solve): ExternalClass {
$object = new ExternalClass();
// as soon as we have our object we have to solve
// self references from data
$solve($object, $data);
// set the required properties
$object->setA($data["a"]);
$object->setB($data["b"]);
// return the deserialized object
return $object;
},
);
Serialization function
The serialization function receives an object as input and must return an array of serializable data. It has the following signature:
/**
* @param object $object The object that must be serialized
* @return array The serialized data
*/
function (object $object): array;
// note: you are free to replace object (first parameter)
// with the actual class name for type hinting
Deserialization function
Unlike serialization, the deserialization of objects requires a little more code in order to make it work with self references or circular references. The function has the following signature:
/**
* @param array $data A reference data that comes from the serialization function
* @param callable $solve A callback used to solve references, must be invoked right after we have the object
* and before we set any properties to it (optional, but recommended)
* @param ReflectionClass $class Class reflection, useful when this function is generic (optional)
* @return object The deserialized object
*/
function (array &$data, callable $solve, ReflectionClass $class): object;
// note: you are free to replace object (return type)
// with the actual class name for type hinting
The deserialization function receives as input a reference to the serialized array and a callback used to solve references. The idea is to invoke the callback as soon as we have the object and before any possible self-reference properties are set to it. We also send the data where to solve the references for the object.
The solve callback has the following signature:
/**
* @param object|null $object The object that was built for deserialization.
* @param mixed $value Usually the data that might contain references to the object (optional)
* @return void
*/
function(?object $object, mixed &$value = null): void;
Default object serializers
By default, we have custom serializers for the following PHP classes:
- enum (cannot be overwritten)
- stdClass (cannot be overwritten)
- Closure (cannot be overwritten)
- ArrayObject
- SplObjectStorage
- SplFixedArray
- SplDoublyLinkedList
- SplStack
- SplQueue
- SplHeap
- SplMaxHeap
- SplMinHeap
- SplPriorityQueue
- WeakMap
- WeakReference
The following internal classes are supported and don’t require custom serializers:
Object boxing
When Opis Closure serializes an object it uses a box where it puts necessary info such as class name and data. At deserialization the data is unboxed and the object reconstructed. This may add a little overhead for some objects that don’t have references to closures or any other data that has a custom serializer.
Here is an example
class NumberPair {
public function __construct(
public int $a,
public int $b
) {}
public function __serialize() : array{
return [$this->a, $this->b];
}
public function __unserialize(array $data) : void{
[$this->a, $this->b] = $data;
}
}
The above class always uses only integers in the serialized data array, so in this case the boxing can be avoided.
If you own the class, you should add the PreventBoxing
attribute.
use Opis\Closure\Attribute\PreventBoxing;
#[PreventBoxing]
class NumberPair {
// ...
}
The other method involves using the prevent_boxing()
function, and can be used on any class.
use Some\Vendor\SomeClass;
use Other\Vendor\SomeOtherClass;
use function Opis\Closure\prevent_boxing;
prevent_boxing(SomeClass::class, SomeOtherClass::class, ...);
All objects that directly or indirectly have references to a closure or any boxed objects, must not prevent boxing.
Below is an example of class that must not prevent boxing because the array might contain an object that is boxed (such as closure).
class MustBeBoxed {
public function __construct(
public array $arbitraryItems
) {}
}
By default, all non-internal objects are boxed. When an object has boxing prevented, Opis Closure will not know about it’s serialized data.