jorge07 / symfony-6-es-cqrs-boilerplate

Symfony 6 DDD ES CQRS backend boilerplate.
MIT License
1.07k stars 187 forks source link

Related objects #89

Closed djkoza closed 5 years ago

djkoza commented 5 years ago

Hi, I have some problems with save and fetch object with related field, but some example: 1) User 2) Stream

Stream has owner User, so mapping:

stream

    <entity name="App\Infrastructure\Stream\Query\Projections\StreamView" table="streams">
        <id name="uuid" type="uuid_binary" column="uuid"/>
        (...)
        <many-to-one field="user" target-entity="App\Infrastructure\User\Query\Projections\UserView" inversed-by="streams">
            <join-column nullable="false" referenced-column-name="uuid" />
        </many-to-one>
    </entity>

user

    <entity name="App\Infrastructure\User\Query\Projections\UserView" table="users">
        <id name="uuid" type="uuid_binary" column="uuid"/>
        (...)
        <one-to-many field="streams" target-entity="App\Infrastructure\Stream\Query\Projections\StreamView" mapped-by="user">
        </one-to-many>
    </entity>

I want to create new stream, related to User. So, I create CreateStreamCommand and Handler, like this:

    public function __invoke(CreateStreamCommand $command): void
    {
        $stream = $this->streamFactory->register($command->uuid, $command->user, $command->parameters);

        $this->streamRepository->store($stream);
    }

    public function __construct(StreamFactory $streamFactory, StreamRepositoryInterface $streamRepository)
    {
        $this->streamFactory    = $streamFactory;
        $this->streamRepository = $streamRepository;
    }

register method from StreamFactory

    public function register(UuidInterface $uuid, UserViewInterface $user, Parameters $parameters): Stream
    {
        return Stream::create($uuid, $user, $parameters);
    }

create method from Stream

    public static function create(UuidInterface $uuid, UserViewInterface $user, Parameters $parameters): self
    {
        $stream = new self();

        $stream->apply(new StreamWasCreated($uuid, $user, $parameters));

        return $stream;
    }

And StreamWasCreated Event

    public function __construct(UuidInterface $uuid, UserViewInterface $user, Parameters $parameters)
    {
        $this->uuid         = $uuid;
        $this->user         = $user;
        $this->parameters   = $parameters;
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public static function deserialize(array $data): self
    {
        Assertion::keyExists($data, 'uuid');
        Assertion::keyExists($data, 'user');
        Assertion::keyExists($data, 'parameters');

        return new self(
            Uuid::fromString($data['uuid']),
            $data['user'],
            Parameters::fromArray($data['parameters'])
        );
    }

    public function serialize(): array
    {
        return [
            'uuid'          => $this->uuid->toString(),
            'user'          => $this->user,
            'parameters'    => $this->parameters->toArray()
        ];
    }

At this step I don't serialize user, and everything is fine. But... When I want to get this Stream record, I see error:

PHP Fatal error:  Uncaught Symfony\Component\Debug\Exception\FatalThrowableError: Argument 2 passed to App\Domain\Stream\Event\StreamWasCreated::__construct() must implement interface App\Domain\User\Query\Projections\UserViewInterface, array given, called in /app/src/Domain/Stream/Event/StreamWasCreated.php on line 51 in /app/src/Domain/Stream/Event/StreamWasCreated.php:32
Stack trace:
#0 /app/src/Domain/Stream/Event/StreamWasCreated.php(51): App\Domain\Stream\Event\StreamWasCreated->__construct(Object(Ramsey\Uuid\Uuid), Array, Object(App\Domain\Stream\ValueObject\Parameters))
#1 /app/vendor/broadway/broadway/src/Broadway/Serializer/SimpleInterfaceSerializer.php(58): App\Domain\Stream\Event\StreamWasCreated::deserialize(Array)
#2 /app/vendor/broadway/event-store-dbal/src/DBALEventStore.php(234): Broadway\Serializer\SimpleInterfaceSerializer->deserialize(Array)
#3 /app/vendor/broadway/event-store-dbal/src/DBALEventStore.php(93): Broadway\EventStore\Dbal\DBALEventStore->deserializeEvent(Array)
#4 /app/vendor/broadway/broadway/s in /app/src/Domain/Stream/Event/StreamWasCreated.php on line 32

I checked second argument, and it is empty array. So I think, problem is in unserialize event data.

So my second try was serialize User object, but then I can't save Stream object, because doctrine thinks user is new object and try to cascade persist.

What is the propper way to work with relationship?

Sorry my english ;)

blixit commented 5 years ago

The error message and the 1st line of your trace tell you what is wrong.

#0 /app/src/Domain/Stream/Event/StreamWasCreated.php(51): 
App\Domain\Stream\Event\StreamWasCreated->__construct(
  Object(Ramsey\Uuid\Uuid),

  Array, // <==== should be of type UserViewInterface

  Object(App\Domain\Stream\ValueObject\Parameters)
)

The 2nd parameter given to the constructor of StreamWasCreated have to be of type UserViewInterface but in your case it's an array.

return new self(
            Uuid::fromString($data['uuid']),
            $data['user'],
            Parameters::fromArray($data['parameters'])
        );

you need to deserialize $data['user']. it's an array.

I will advice you to use Id/Uuid instead of UserviewInterface and add other user informations when running a projection into the read model.

Here is my trick to handle serialization of fields independantly : https://github.com/jorge07/symfony-4-es-cqrs-boilerplate/issues/87

But according to @jorge07 it's better to let a serializer handle it for you (jms_serializer, symfony/serializer, ...)

jorge07 commented 5 years ago

Hi @djkoza

Your StreamWasCreated event is passing an array as Object. You need to deserialise User before pass it to the constructor.

  /**
     * @throws \Assert\AssertionFailedException
     */
    public static function deserialize(array $data): self
    {
        Assertion::keyExists($data, 'uuid');
        Assertion::keyExists($data, 'user');
        Assertion::keyExists($data, 'parameters');

        return new self(
            Uuid::fromString($data['uuid']),
-          $data['user'],
+          User:: deserialize($data['user']),
            Parameters::fromArray($data['parameters'])
        );
    }

Also, I do not recommend use Projections in DomainEvents. A Projection is just a representation of the model, not the model itself. Use a domain object instead.

djkoza commented 5 years ago

Thanks a lot for your reply @blixit @jorge07

you need to deserialize $data['user']. it's an array.

When I call CreateStreamCommand this is UserView object. But when I call (after save) to fetch

$stream = $this->streamRepository->get($command->uuid);

Then I get mentioned error and in this moment $data['user'] it's an array - empty.

When I serialize with UserView::serialize()

        return [
            'uuid'        => $this->getId(),
            'credentials' => [
                'email' => (string) $this->credentials->email,
            ],
        ];

I have got error from doctrine about cascade persisting UserView. So, as I mentioned

So my second try was serialize User object, but then I can't save Stream object, because doctrine thinks user is new object and try to cascade persist.

@blixit

I will advice you to use Id/Uuid instead of UserviewInterface and add other user informations when running a projection into the read model.

I tried this solution, but where is the best place code to fetch User from repository and assign them to Stream on create or update record? This must have happened before persist, because

doctrine thinks user is new object and try to cascade persist.

@jorge07

Also, I do not recommend use Projections in DomainEvents. A Projection is just a representation of the model, not the model itself. Use a domain object instead.

if I understand correctly, my relation is needed to rebuild: "target-entity" not point to Projections, but to Domain objects?

And how I can get User Domain Object, not Projection?

Newbie questions, I'm new in DDD

Thanks a lot ;)

jorge07 commented 5 years ago

I'll create a branch with a similar use case will be easier for others to look at this and see how to start or process with things like that.

blixit commented 5 years ago

I tried this solution, but where is the best place code to fetch User from repository and assign them to Stream on create or update record

When you are creating your object I guess you already know what user to attach to it. You could fetch the user when handlind the command (inside the ~CreateStreamCommand~ CreateStreamCommandHandler).

djkoza commented 5 years ago

@jorge07 this will be very helpful, I will be patient. @blixit ok, that's fine - but what with doctrine problem? I can't persist and flush Stream object because related User object is trying to persist too. As we know, User already exist and database fail to execute this insert (create) or update command.

blixit commented 5 years ago

related User object is trying to persist too

this is a known issue with doctrine. you need to properly set your 'cascade' properties (on persist, on update, ...). it's not due to es-cqrs

djkoza commented 5 years ago

@blixit I found some solutions, 1) merge() via entity manager 2) fetch User (related object) (on deserialize) from orm and call setUser on Stream object 3) use JMSSerializerBundle and Doctrine object constructor

What is the best way to deal with this problem?

blixit commented 5 years ago

i use to use the 2nd solution and the 1st one in this order.

djkoza commented 5 years ago

@blixit ok, but some question - how can I get repository in StreamWasCreated event or in StreamView Projection? We don't call this classes via service, so dependency injection dosent work. How to deal with that? I want to fetch related object in deserialize operation

blixit commented 5 years ago

How can I get repository in StreamWasCreated event or in StreamView Projection I think it's a bad design. (Maybe you mean the event handler.)

I think the goal of the :

You should not call repository at this place. I suggest you to fetch related objects from the commandhandler. For instance:

jorge07 commented 5 years ago

Hi @blixit @djkoza,

I made this branch with an example of user relations and a way to minimise the couple between bounded contexts.

jorge07 commented 5 years ago

Here the commit reference

jorge07 commented 5 years ago

I think we can close it. Feel free to reopen it if needed it.