Problem with abstract nested object
I have some abstract classes that represent my common "base" value objects: for example ULID, String, Email, ... I typically extend these abstract classes with one more appropriate for the context of my value object: for example CustomerId, CustomerName, CustomerEmail, ... Finally I use these Value Objects inside my entity, my other Value Object and so on.
abstract class UlidValueObject
{
final public function __construct(public readonly string $value)
{
if (!Ulid::isValid($value)) {
throw new InvalidArgumentException('bla bla bla');
}
}
// public static function generate(): static
// public function equalTo(self $other): bool
// public function __toString(): string
}
class CustomerId extends UlidValueObject
{
}
class Customer
{
public function __construct(
public CustomerId $id,
public CustomerEmail $email,
// ...
) {
}
}
When I serialize the Customer object I get:
{"id": {"value": "01HBQXG72X9KF1PF6KSJXPW26W"}, "email": {"value": "[email protected]"}}
but when I try to deserialize it I get this error
Cannot initialize readonly property ...\UlidValueObject::$value from scope ...\CustomerId"
How can I fix it?
My environment
PHP 8.2
Erf. This is a PHP limitation. readonly properties are private-write, and you cannot change that. That means you cannot write to them even from a child class. IMO this is a design flaw in readonly and one of the key reasons we should have gone straight to asymmetric visibility instead.
Other than redeclaring the property in child classes, I don't really know of a way around it without improvements to PHP itself. (Ilija and I proposed asymmetric visibility to do just that, and it was rejected.)
Just to come along and show an example of what @Crell was saying and pointing out some limitations (to save everyone else some trouble), here's a simplified example:
require_once __DIR__.'/../vendor/autoload.php';
readonly class base {
public function __construct(public string $name, public string $id) {}
}
readonly class tom extends base {
public function __construct(string $id) {
parent::__construct('tom', $id);
}
}
$tom = new tom('friend');
$serializer = new \Crell\Serde\SerdeCommon();
$result = $serializer->serialize($tom, 'json');
var_dump($result);
$same = $serializer->deserialize($result, 'json', tom::class);
var_dump($same);
Which gives the following output:
php src/test.php
string(28) "{"name":"tom","id":"friend"}"
PHP Fatal error: Uncaught Error: Cannot initialize readonly property base::$name from scope tom
It isn't enough to just declare the child class with constructor promotion:
readonly class tom extends base {
public string $name;
public function __construct(public string $id) {
parent::__construct('tom', $id);
}
}
It will now fail with:
PHP Fatal error: Uncaught Error: Cannot modify readonly property tom::$id
You must redeclare every property in the class body:
readonly class tom extends base {
public string $name;
public string $id;
public function __construct(string $id) {
parent::__construct('tom', $id);
}
}
Which now gives the correct output:
string(28) "{"name":"tom","id":"friend"}"
object(tom)#30 (2) {
["name"]=>
string(3) "tom"
["id"]=>
string(6) "friend"
}
Also, I'd like to point out that you cannot use a trait to reduce duplication ... you have to spell it out every single time. If someone knows how to use a trait, that would be fantastic, but I couldn't figure out how.
Closing, as this is not something that can be solved by Serde.