foundry icon indicating copy to clipboard operation
foundry copied to clipboard

[Repository] implement a negation operator

Open seb-jean opened this issue 8 months ago • 8 comments

Hi :),

I would like to optimize the code below, especially the $users[$i - 1] part.

  1. The idea behind my optimization is to randomly get a user, excluding the user $userTest. Perhaps adding a function like exclude(['email' => '[email protected]']) by adding attributes would be good.
  2. It's also important that when I get the users, they all have to be unique. I have a uniqueness index on the visitor_id and visited_id fields. Is this already implemented as a feature?
// src/Entity/Visit.php

#[ORM\UniqueConstraint(fields: ['visitor', 'visited'])]
// src/DataFixtures/AppFixtures.php

$users = UserFactory::createMany(100);

$userTest = UserFactory::createOne([
    'email' => '[email protected]',
]);

VisitFactory::createMany(100, fn ($i) => [
    'visitor' => $users[$i - 1],
    'visited' => $userTest,
]);

VisitFactory::createMany(100, fn ($i) => [
    'visitor' => $userTest,
    'visited' => $users[$i - 1],
]);

seb-jean avatar Mar 26 '25 14:03 seb-jean

Hi @seb-jean

there are two different topics here

about an exclude() method

I'm a bit torn about this: supporting a not operator (as well as a or operator) would be nice, but on the other hand, I don't see any easy way to do this: we're relying on doctrine/persistence for all Factory::findXXX() methods, and it does not allow this. And I don't think this feature is worth the complexity it would involve (I think that we don't want to rely on ORM query builder stuff in Foundry)

But I'm not reluctant to add this feature if a good idea comes up.

By the way, you can still use a custom method from your repo:

UserFactory::createMany(100);

$userNotKevin = UserFactory::repository() // gives you a `\Zenstruck\Foundry\Persistence\RepositoryDecorator`
    ->inner() // gives you the repository defined in `#[ORM\Entity(repository="..."]`
   ->randomButExclude('[email protected]'); // custom method in your app

about the "uniqueness"

This sounds even more challenging 🙃 Maybe a method PersistentObjectFactory::unique() (or PersistentObjectFactory::repository()->unique() ?) could be possible? but this would definitely require to support a not() operator 🤔 Moreover that random with doctrine and/or SQL is not a so simple problem (by the way, Foundry has a performance problem with random).

And, this would mean that in your case, you'd make 100 requests to the db, which maybe isn't what you want to do.

Still, as for the "not" stuff, if a good idea pops up, there is no problem to add the feature.

Note that in Foundry 2.4, thanks to the new distribute() method, you'll be able to do something like this:

$users = UserFactory::createMany(100);

$userTest = UserFactory::createOne([
    'email' => '[email protected]',
]);

VisitFactory::new(['visited' => $userTest])
	->many(100)
	->distribute('visitor', $users)
    ->create();

VisitFactory::new(['visitor' => $userTest])
	->many(100)
	->distribute('visited', $users)
    ->create();

on more thing: you should also use flush_after helper, this would improve the performances of your queries.

ping @kbond for some insights

nikophil avatar Mar 26 '25 18:03 nikophil

Thank you very much for your reply.

seb-jean avatar Mar 28 '25 18:03 seb-jean

about the uniqueness, maybe something like this would be ok?

VisitFactory::createMany(100, [
    'visitor' => $userTest,
    'visited' => unique(fn() => UserFactory::createMany(100)),
]);

the unique() helper could take a callable (which it would call only once) or an array, and it would return only unique values. But TBH I don't see the real advantage compared to using distrubute() 🤔

nikophil avatar Mar 30 '25 12:03 nikophil

Indeed, distribute() could be used to get unique users. However, is it possible to use distribute() with createMany()?

seb-jean avatar Mar 31 '25 18:03 seb-jean

not directly with createMany(), but this method exists (or actually, it WILL exist, not released yet) on FactoryCollection class:

// both of these lines are equivalent:
VisitFactory::createMany();
VisitFactory::new()->many()->create();

// and `->many()` returns a `FactoryCollection`, so you can call:
VisitFactory::new()->many()->distribute($var)->create();

nikophil avatar Apr 01 '25 07:04 nikophil

Ok, thanks for the information!

seb-jean avatar Apr 01 '25 15:04 seb-jean

I looked for several solutions and found a solution that is interesting:

// src/DataFixtures/AppFixtures.php

$users = UserFactory::createMany(100);

$userTest = UserFactory::createOne([
    'email' => '[email protected]',
]);

VisitFactory::createSequence(
    function () use ($users, $userTest) {
        foreach ($users as $user) {
            yield ['visitor' => $userTest, 'visited' => $user];
            yield ['visitor' => $user, 'visited' => $userTest];
        }
    }
);

This allows me to no longer use the index system: $users[$i - 1] but also to bring the 2 createMany blocks together.

seb-jean avatar Apr 03 '25 09:04 seb-jean

haha I did not thought about yielding twice in a sequence, that's clever!

nikophil avatar Apr 03 '25 09:04 nikophil

I'm closing this, since I don't think we will ever implement a way to have a negatable operator, if doctrine does not provide it out of the box

nikophil avatar Jul 06 '25 17:07 nikophil