foundry icon indicating copy to clipboard operation
foundry copied to clipboard

[Help] How to create rich domain entities?

Open nikophil opened this issue 3 years ago • 11 comments

Hello,

that's not an issue, but more a request for help.

I have some "rich" domain entities (opposite of anemic entities) and I struggle to create them with foundry. Here is a simple example:

// Post.php
#[ORM\Entity]
class Post
{
    #[ORM\Column]
    private string $title;

    #[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'post')]
    private iterable $comments;

    public function __construct(string $title);

    public function addComment(Comment $comment): void;

    public function getComments(): iterable<Comments>
}

#[ORM\Entity]
class Comment
{
    #[ORM\ManyToOne(targetEntity: Event::class, inversedBy: 'slots'), ORM\JoinColumn(nullable: false)]
    private Post $post;

    #[ORM\Column]
    private string $body;

    public function __construct(Post $post, string $body);
}

Comment::$post property can never be modified in the app, thus I have zero need to add a setter on this property.

Is there a way to create a post with some comments with foundry without hacking the code?

thanks for your answer

nikophil avatar Sep 29 '22 17:09 nikophil

Hey!

Rich entities is something I've been trying to build myself. I don't like entities that are constructed in an invalid state and with unnecessary setters/getters.

With your example above, my first thought is using Instantiator::alwaysForceProperties()->withoutConstructor(). Have you looked at that?

I always use this in my apps. I tend to like the idea of creating fixtures based on their database values rather than having them perfectly created via constructor/methods. I know this perhaps is controversial but this is how doctrine works when it hyrdates objects from the db.

kbond avatar Sep 30 '22 14:09 kbond

thanks for you answer!

I think this may be the way to go, but... Comment::$post is mandatory is the db... so how could I tell foundry to hydrate it somehow?

nikophil avatar Sep 30 '22 14:09 nikophil

To clarify, you are tying to do the following?

PostFactory::createOne([
    'title' => 'my title',
    'comments' => CommentFactory::new()->many(5),
]);

And in your CommentFactory::getDefaults() you return ['post' => PostFactory::new()]?

kbond avatar Sep 30 '22 15:09 kbond

I've confirmed this is, I think, a bug.

I've created a reproducer to demonstrate (commit).

Basically, you cannot currently use always_force_properties with doctrine collections (it tries to set as an array). I'm thinking a fix would be having FactoryCollection return an ArrayCollection. This would be a BC break so we'd have to create a deprecation layer.

But @nikophil, can you confirm we're on the same page?

kbond avatar Oct 03 '22 15:10 kbond

Hello @kbond

sorry, I have not seen the notification. This is indeed my problem. But how could we be sure the original Post entity created will be the one used by CommentFactory::getDefaults()?

In your reproducer you're calling a method Comment::setPost() which does not exist

https://github.com/kbond/symfony-reproducer/commit/715ae2ee9d3e79dc4675e486d6523b10b6b160c0#diff-c1d37559aa251754682b2e972c2eef2018b93e96aa35730a6a2e73363b210fc4R52

nikophil avatar Oct 04 '22 07:10 nikophil

But how could we be sure the original Post entity created will be the one used by CommentFactory::getDefaults()?

I think that might work currently but I need to confirm.

In your reproducer you're calling a method Comment::setPost() which does not exist

Ah yes, in your example in the pr description above, how does your addComment method work?

kbond avatar Oct 04 '22 12:10 kbond

something simple such as

    public function addComment(Comment $comment): self
    {
        if (!$this->comments->contains($comment)) {
            $this->comments->add($comment);
        }

        return $this;
    }

nikophil avatar Oct 04 '22 13:10 nikophil

Ah ok, got ya

kbond avatar Oct 04 '22 14:10 kbond

@nikophil, do you also have a removeComment() method on your Post?

kbond avatar Jan 09 '23 20:01 kbond

This could happen, indeed

nikophil avatar Jan 09 '23 20:01 nikophil

I discovered that your example in this issue description should work as expected now under the following conditions:

  1. Post::removeComment() exists (the property accessor component requires this method to use adders)
  2. Not using cascade: persist on Post::$comments

kbond avatar Jan 09 '23 20:01 kbond