Disclaimer: If no inheritance is involved or all properties are public or protected, you can use one of many solutions provided before.
The solution discussed here is designed to work with inheritance and private properties. It's specially useful to remove injected
dependencies.
Basically use __sleep()
to exclude properties from serialization.
But we need a way to extract all property names of $this
. Use __wakeup()
to re-establish those lost connections/data.
PHP Serialization of Objects with Inheritance and Private Properties
If __sleep()
is present in your class and __serialize()
is not, serialize()
uses __sleep()
to grab a list of properties which should be serialized. The list is one-dimensional but has to be a specific format to
determine which private property belongs to which class. This is the format, where \0
are null chars:
[
"publicProperty",
"\0*\0protectedProperty",
"\0ClassName\0privateProperty",
]
To bring the property list into the correct format, we found two solutions.
Note: __sleep()
only needs to be implemented on the parent class.
Use (array)
cast
The array cast results in an array with all properties of an object and with the correct keys.
Note: This solution is still quite error-prone especially during refactoring, since excluded property names are hardcoded as strings.
class ParentClass {
private $parentProperty1;
private $parentProperty2;
public function __construct() {
$this->parentProperty1 = 'Parent Property 1';
$this->parentProperty2 = 'Parent Property 2';
}
public function __sleep() {
$excludedProperties = [
"\0ParentClass\0parentProperty1",
];
$properties = (array)$this;
return array_filter(array_keys($properties), function ($propertyName) use ($excludedProperties) {
return !in_array($propertyName, $excludedProperties);
});
}
}
class ChildClass extends ParentClass {
private $childProperty3;
public function __construct()
{
parent::__construct();
$this->childProperty3 = 'Child Property 3';
}
}
$child = new ChildClass();
var_dump($child);
$serialized = serialize($child);
var_dump($serialized);
$child = unserialize($serialized);
var_dump($child);
Result
object(ChildClass)#1 (3) {
["parentProperty1":"ParentClass":private] => string(17) "Parent Property 1"
["parentProperty2":"ParentClass":private] => string(17) "Parent Property 2"
["childProperty3":"ChildClass":private] => string(16) "Child Property 3"
}
string(141) "O:10:"ChildClass":2:{s:28:" ParentClass parentProperty2";s:17:"Parent Property 2";s:26:" ChildClass childProperty3";s:16:"Child Property 3";}"
object(ChildClass)#2 (3) {
["parentProperty1":"ParentClass":private] => NULL
["parentProperty2":"ParentClass":private] => string(17) "Parent Property 2"
["childProperty3":"ChildClass":private] => string(16) "Child Property 3"
}
Use Reflection and Attributes (Recommended)
Trade speed for elegance and readability. Using Reflections is about 2x slower according to some simple, not-representative
benchmarks listed below. The serialization of a complex object in our production code takes about 60 microseconds (which isn't
representative either), just so you have a baseline.
The reflection loops over all properties of this class and all parent classes. It checks if the property is private and builds the
property names accordingly.
#[Attribute]
class DoNotSerialize {}
class ParentClass {
private $parentProperty1;
#[DoNotSerialize]
private $parentProperty2;
public function __construct(
#[DoNotSerialize] private $parentProperty3,
) {
$this->parentProperty1 = 'Parent Property 1';
$this->parentProperty2 = 'Parent Property 2';
}
public function __sleep() {
$props = [];
$reflectionClass = new ReflectionClass($this);
do {
$reflectionProps = $reflectionClass->getProperties();
foreach ($reflectionProps as $reflectionProp) {
// $reflectionProp->setAccessible(true); // not needed after PHP 8.1
if (empty($reflectionProp->getAttributes(DoNotSerialize::class)) && !$reflectionProp->isStatic()) {
$propertyName = $reflectionProp->getName();
// PHP uses NUL-byte prefixes to represent visibility in property names
if ($reflectionProp->isPrivate()) {
$propertyName = "\0" . $reflectionProp->getDeclaringClass()->getName() . "\0" . $propertyName;
} elseif ($reflectionProp->isProtected()) {
$propertyName = "\0*\0" . $propertyName;
}
$props[] = $propertyName;
}
}
$reflectionClass = $reflectionClass->getParentClass();
} while ($reflectionClass);
return $props;
}
}
class ChildClass extends ParentClass {
private $childProperty4;
public function __construct()
{
parent::__construct('Parent Property 3');
$this->childProperty4 = 'Child Property 4';
}
}
$child = new ChildClass();
var_dump($child);
$serialized = serialize($child);
var_dump($serialized);
$child = unserialize($serialized);
var_dump($child);
Result
object(ChildClass)#1 (4) {
["parentProperty1":"ParentClass":private] => string(17) "Parent Property 1"
["parentProperty2":"ParentClass":private] => string(17) "Parent Property 2"
["parentProperty3":"ParentClass":private] => string(17) "Parent Property 3"
["childProperty4":"ChildClass":private] => string(16) "Child Property 4"
}
string(141) "O:10:"ChildClass":2:{s:26:" ChildClass childProperty4";s:16:"Child Property 4";s:28:" ParentClass parentProperty1";s:17:"Parent Property 1";}"
object(ChildClass)#6 (4) {
["parentProperty1":"ParentClass":private] => string(17) "Parent Property 1"
["parentProperty2":"ParentClass":private] => NULL
["parentProperty3":"ParentClass":private] => NULL
["childProperty4":"ChildClass":private] => string(16) "Child Property 4"
}
Simple Benchmark
(array)
cast
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
$child = new ChildClass();
$serializedArray = serialize($child);
}
$end = microtime(true);
$timeArray = ($end - $start) * 1000;
echo "Time: $timeArray ms" . PHP_EOL;
Result: Time: 587.77904510498 ms
Reflection
$start = microtime(true);
for ($i = 0; $i < 1000000; $i++) {
$child = new ChildClass();
$serializedReflection = serialize($child);
}
$end = microtime(true);
$timeReflection = ($end - $start) * 1000;
echo "Time: $timeReflection ms" . PHP_EOL;
Result: Time: 1218.0068492889 ms
Other Approaches
get_object_vars()
returns an associative array with all properties of an object in scope. This is a problem because it breaks
serialization of classes with inherited private properties.
This seems to be the recommended approach. However, it comes with a large programming overhead. You would have to implement this
behaviour on every class in your hierarchy. Plus we want PHP to do the serialization, so we don't have to __unserialize()
manually.
Example grabbed from PHP RFC: New custom object serialization mechanism
class A {
private $prop_a;
public function __serialize(): array {
return ['prop_a' => $this->prop_a];
}
public function __unserialize(array $data) {
$this->prop_a = $data['prop_a'];
}
}
class B extends A {
private $prop_b;
public function __serialize(): array {
return [
'prop_b' => $this->prop_b,
'parent_data' => parent::__serialize(),
];
}
public function __unserialize(array $data) {
parent::__unserialize($data['parent_data']);
$this->prop_b = $data['prop_b'];
}
}
__sleep
will not work with private properties in the parent class, so it's only helpful as long as the inheritance is not involved. – Virilism