Serde icon indicating copy to clipboard operation
Serde copied to clipboard

Problem with abstract nested object

Open SiMoSiMo2 opened this issue 2 years ago • 3 comments

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

SiMoSiMo2 avatar Dec 07 '23 11:12 SiMoSiMo2

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.)

Crell avatar Dec 07 '23 21:12 Crell

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.

withinboredom avatar Jan 14 '24 14:01 withinboredom

Closing, as this is not something that can be solved by Serde.

Crell avatar Feb 09 '24 20:02 Crell