Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feature] allow creating "rich domain models" #312

Open
wants to merge 1 commit into
base: 1.x
Choose a base branch
from

Conversation

kbond
Copy link
Member

@kbond kbond commented Oct 12, 2022

Possible solution for #305. When forcing properties with the Instantiator, covert array's to ArrayCollection's if the typehint demands it.

Not a huge fan of this solution... I think I'd prefer FactoryCollection generate an ArrayCollection and then it can be set directly. I'll explore this option.

src/Instantiator.php Outdated Show resolved Hide resolved
@X-Coder264
Copy link

@kbond Is there anything left to be done before this can be merged?

We are currently trying to migrate to Foundry but this is blocking us.

As CommentFactory in our case has 'post' => PostFactory::new(), in its getDefaults method (so that the CommentFactory::createOne(); call can work) the piece of code below creates two post records and one comment record in the database (instead of the expected one post record with one comment). Plus the created comment record gets created for the other post, not the one that actually gets returned by the post factory call.

        $post = PostFactory::createOne([
            'comments' => CommentFactory::new()->many(1),
        ])->object();

       $post->getComments()->isEmpty(); // currently true, the expected result is false

@nikophil
Copy link
Member

nikophil commented Jan 4, 2023

Hi @X-Coder264

Is there anything left to be done before this can be merged?

we would like to try another way of doing this: FactoryCollection::create() could return directly an ArrayCollection. We could give a try in the next few days if this feature is missing for someone

@kbond
Copy link
Member Author

kbond commented Jan 4, 2023

Hey @X-Coder264, I'd like finish this one up shortly, just getting back from holidays.

@kbond kbond marked this pull request as ready for review January 9, 2023 16:27
@kbond
Copy link
Member Author

kbond commented Jan 9, 2023

I think this is ready as-is. @X-Coder264, can you try this out in your project?

There's some things I'd still like to do:

  1. Split Instantiator into separate instantiator/hydrator (Handling factory methods using instantiateWith #388)
  2. Use symfony/property-info to detect doctrine collections (right now the prop needs to be type-hinted as Collection).
  3. Add FactoryCollection::asDoctrineCollection() or something like that.

But these can be added in the future (probably when we split the instantiator).

@kbond
Copy link
Member Author

kbond commented Jan 9, 2023

@nikophil, any idea on the CI failure? Related to #393 somehow?

@nikophil
Copy link
Member

nikophil commented Jan 9, 2023

very strange 🤔
I already had these errors in #393 but they were fixed by adding a condition on the migrations related to PHP 8.1
https://github.com/zenstruck/foundry/blob/1.x/tests/Fixtures/Migrations/Version20230109134337.php#L19

@nikophil
Copy link
Member

nikophil commented Jan 9, 2023

would you mind rebasing on 1.x?

@kbond
Copy link
Member Author

kbond commented Jan 9, 2023

would you mind rebasing on 1.x?

Done but still a problem. I see the failures from #393 that had the same issue, was this the solution for that one?

@X-Coder264
Copy link

@kbond I've tried it out and for some reason I'm still getting the same old/wrong behavior even though I can see that the new $value = new ArrayCollection($value); code path gets successfully triggered when I put a Xdebug breakpoint there. I can't see any meaningful difference between your test case and my code (except that I use cascade persist, orphan removal and fetch extra lazy on my mapping but none of those should make a difference) 🤔

EDIT: It does make a difference.

This works (same scenario as your test case):

    /**
     * @ORM\OneToMany(targetEntity="Comment", mappedBy="post")
     */
    private Collection $comments;
    /**
     * @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
     * @ORM\JoinColumn(nullable=false)
     */
    private Post $post;

This doesn't work as expected:

    /**
     * @ORM\OneToMany(targetEntity="Comment", mappedBy="post", cascade={"persist"}, orphanRemoval=true, fetch="EXTRA_LAZY")
     */
    private Collection $comments;
    /**
     * @ORM\ManyToOne(targetEntity="Post", inversedBy="comments", cascade={"persist"})
     * @ORM\JoinColumn(nullable=false)
     */
    private Post $post;

@X-Coder264
Copy link

X-Coder264 commented Jan 9, 2023

Ok, found the issue. Removing the cascade persist "fixes" the issue. So if I change my mapping to this it works as expected:

    /**
     * @ORM\OneToMany(targetEntity="Comment", mappedBy="post", orphanRemoval=true, fetch="EXTRA_LAZY")
     */
    private Collection $comments;

I'm not sure why does cascade persist change anything, the behavior with or without cascade persist should be the same (getting back only one post with X comments).

@kbond
Copy link
Member Author

kbond commented Jan 9, 2023

Ok, thanks for testing and finding this edge case. I'll create a test case that uses cascade: persist and see if I can figure out why it doesn't work as expected.

There are some nuances with cascade persist that I don't fully understand (as I never use this feature myself). These are accounted for elsewhere in foundry but I didn't write this code. Maybe something with one of these is causing the problem.

@kbond
Copy link
Member Author

kbond commented Jan 9, 2023

I found the issue but not sure yet how to solve:

When NOT using cascade persist, the instantiation of the comment collection is done after persisting the post. The comments are instantiated with the previously created post (this overrides the default post so the extra post per comment aren't created).

When using cascade persist, the collection needs to be instantiated before creating the post so I don't have a chance to set the post (as it isn't instantiated yet).

/**
* @test
*/
public function can_create_factories_for_rich_domain_models(): void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes and adds the comments via RichPost::setComment() (property-accessor).

/**
* @test
*/
public function can_create_factories_for_rich_domain_models_using_force(): void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test passes and adds the comments by forcing RichPost::$comments.

/**
* @test
*/
public function can_create_factories_for_cascade_rich_domain_models(): void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fails due to the cascade persist on CascadePost::$comments.

/**
* @test
*/
public function can_create_factories_for_cascade_rich_domain_models_using_force(): void
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fails due to the cascade persist on CascadePost::$comments.

@kbond
Copy link
Member Author

kbond commented Jan 9, 2023

I now test using rich entities with and without alwaysForceProperties() and this all works as expected.

The same tests using cascade persist do not pass. Still not sure how to solve. We're kind of in a chicken-egg situation: to create the comment, we need the post but to create the post, we need the comment...

@kbond
Copy link
Member Author

kbond commented Jan 10, 2023

I think when we split the instantiator into an instantiator and a hydrator we might be able to make cascade: persist work here.

We're kind of in a chicken-egg situation: to create the comment, we need the post but to create the post, we need the comment...

We could instantiate the post (w/o hydrating), then instantiate and hydrate the comments (using the post object), then hydrate the post with the created comments. Complicated but doable I think.

@nikophil
Copy link
Member

I think we really should have this work with cascade={"persist"} since it is really common to have this

@kbond kbond mentioned this pull request Jan 10, 2023
3 tasks
@kbond
Copy link
Member Author

kbond commented Jan 10, 2023

I think we really should have this work with cascade={"persist"} since it is really common to have this

Agreed, I'm hoping #395 will help solve.

@nikophil
Copy link
Member

@kbond I've found why some tests not related to this PR are failing. Could you add this code to all migration which does not have it?

    public function isTransactional(): bool
    {
        return false;
    }

Migrations are only tested without DAMA, then they are only tested for "php8.0/sf4.4/lowest" and "php8.2/sf6.2/highest" but I don't understand why it does not fail in the second scenario... maybe doctrine/migration is clever enough to know transaction mode should be disabled in certain cases?

I'm considering to add a custom PHPStan rule since it's been multiple times I've lost time on this.

@kbond kbond mentioned this pull request Feb 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants