-
-
Notifications
You must be signed in to change notification settings - Fork 60
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
Entity cascading unexpected behavior #179
Comments
Does it work if you create the factories manually on their own? Create C, create A, then create B with these? I find using foundry for one-to-many a bit finicky for complex scenarios. I tend to favor creating many-to-one's only. |
It didn't for A and B, it's ok for C. In this case, C can live alone. But a B can't exists without a A, and a A can't exists without at least a B. All work without the prePersist listener that generate the reference. Because to generate the A reference the listener get the first B, then the attached C. And to define the B reference, get the C then the A. A and B must be persisted together with a pre-existing C or by saving the C to. Apparently, if I have correctly understand, foundry save the A entity in database (persist and flush) but have not yet added the B to the entity (there is no link, the adder is never called before), then when the listener call the getBs() method, it return an empty collection. This is the real problem, because in this case we just need A have B and B have C objects, to do the reference. After, we can save the first without B and C for exemple. I can see that to in: $a= AFactory::new()
->instantiateWith(function(array $attibutes, string $class): object {
dd($attibutes);
})
->create()
; The dumped array has an empty array for the Collection of B. |
In your app where it works correctly, are you creating/persisting all the entities and then calling I think this is related to #37. Are you able to test with #84. This allows you to create all your factories inside a callback. When they are created they are not immediately flushed. A single flush occurs after the callback has run. #84's description has an example on how to use. |
@kbond In the app, I just fush and persist one time, the A class, then it cascade persist B that cascade persist C. |
Ah, yes, the cascade persist - I'm not very familiar with that doctrine feature but I think that PR could still help. |
Yes, I try it. For the moment it return to me an error about auto refreshing:
Related to the Factory::new() inside, I'll try to disable refreshing globally to check, then if it works, by disabled it only for that part. |
By disabling auto-refreshing it works well for that auto-refresh error. But, return the same error that before, the Collection in A class is still empty. In the Factory, when we define attributes, and an attributes call a Factory, this one is called, procces its attributes, execute persist() then flush() on it, then comeback to the initial factory and continue to resolves its attributes, that's it ? When it find a |
I've fixed this in the PR - disabling auto-refresh within the callback is no longer required.
Yes (but within the callback in #84
Did you make any changes to the default instantiation of factories? If not, then the adder should be called (by What happens when a I know this is a bit of an ask but, can you create a minimal reproducer? Even better would be a failing test but I can use a reproducer to build a failing test myself. |
I investigate more, because in fact I have a collection that is filled with adder called and the other not. I check in my own code if something can do that. Else, the only difference is that one is a Collection of a class with only scalar attributes, than the other is a Collection of a class with relation to other entities, and have Collection to. |
How is the other's collection being set |
Sorry, I didn't express myself well, both use adder. I just want to say for a collection the adder is called, but not for the other. ProbeFactory.php protected function getDefaults(): array
{
return [
//...
'usages' => UsageFactory::new()->many(1),
//...
'probeAcoustics' => ProbeAcousticFactory::new()->withoutProbe()->many(1),
];
} Usages contains a Collection of persisted object, but the probeAcoustics is empty. ProbeAcousticFactory.php protected function getDefaults(): array
{
return [
'acoustic' => AcousticFactory::new(),
'probe' => ProbeFactory::new(),
];
}
public function withoutProbe(): self
{
return $this->addState([
'probe' => null,
]);
} |
$a= ProbeFactory::new()
->instantiateWith(function(array $attibutes, string $class): object {
dd($attibutes['probeAcoustics']); // THIS is [] (an empty array)?
})
->create()
; |
Maybe a bug in Can you try commenting out these lines and see if that works? |
I check it tomorrow, and say you what I find. |
Yep it return an empty array. I've commented lines in But, inevitably break all other parts because it create all dependencies and flush them incompletly (no added to the one that "invoke" them). This is tricky because all work well, all sub Entities are created on demand, persisted and flushed, then after linked to the parent. In case of revert relation, the link is done in prePersist actually. This problem only come from eventListener used in the application, when this one need to access a revertCollection. (In the exemple bellow between Usage collection and ProbeAcoustic collection is here: Usage is a ManyToMany relation owned by Probe, but ProbeAcoustic is a OneToMany relation on Probe); that's why it's escaped in the Factory. But by escaping it, all inversed relation are empty on persist (not totally sure of having all understand). |
I thought about it for a while, I think there is 2 possibilities of creating the entity:
I take a look about it, and see if I find an idea of how to do that. Normally it should solve the issue, and be more realistic with how the fixture is created, compared to how this same entity(ies) are created from the application logic. |
Yeah, I thought this might cause an issue with your other entities. I can try to create a failing test with cascade persist when I have a chance. I still think this is related to the cascade. |
A try to support relation with cascade persist: create the collection of Proxy, and don't persist them: class Factory
{
// ...
/** @var bool */
private $persist = true;
/** @var bool */
private $cascadePersisted = false;
// ...
/**
* @param array|callable $attributes
*
* @return Proxy|object
*
* @psalm-return Proxy<TObject>
*/
final public function create($attributes = []): Proxy
{
//...
if (!$this->isPersisting() || true === $this->cascadePersisted) {
return $proxy;
}
//...
}
//...
final public function setCascadePersisted(bool $cascadePersisted): self
{
$this->cascadePersisted = $cascadePersisted;
return $this;
}
//...
private function normalizeCollection(FactoryCollection $collection): array
{
$field = $this->inverseRelationshipField($collection->factory());
$cascadePersisted = $this->hasCascadePersist($collection->factory(), $field);
if ($this->isPersisting() && $field && false === $cascadePersisted) {
$this->afterPersist[] = static function(Proxy $proxy) use ($collection, $field) {
$collection->create([$field => $proxy]);
$proxy->refresh();
};
// creation delegated to afterPersist event - return empty array here
return [];
}
return $collection->all($cascadePersisted);
}
private function hasCascadePersist(self $factory, ?string $field): bool
{
if (null === $field) {
return false;
}
$collectionClass = $factory->class;
$factoryClass = $this->class;
$collectionMetadata = self::configuration()->objectManagerFor($collectionClass)->getClassMetadata($collectionClass);
$classMetadataFactory = self::configuration()->objectManagerFor($factoryClass)->getMetadataFactory()->getMetadataFor($factoryClass);
// Find inversedBy key
$inversedBy = $collectionMetadata->associationMappings[$field]['inversedBy'] ?? null;
// Find cascade metatada for the inversedBy field
$cascadeMetadata = $classMetadataFactory->associationMappings[$inversedBy]['cascade'] ?? [];
return in_array('persist', $cascadeMetadata, true);
}
} final class FactoryCollection
{
/**
* @return Factory[]
*
* @psalm-return list<Factory<TObject>>
*/
public function all(bool $cascadePersisted = false): array
{
/** @psalm-suppress TooManyArguments */
return \array_map(
function() use ($cascadePersisted) {
$cloned = clone $this->factory;
$cloned->setCascadePersisted($cascadePersisted);
return $cloned;
},
\array_fill(0, \random_int($this->min, $this->max), null)
);
}
} See to use it on no collection relation:
|
That's inline with what I was thinking except, instead of having to manually set, detect from the mapping. Did the above work for your use case? |
Yep, this solve my problem, and do not break others. I work on a way to enable it on owner side to, then do a PR for it. |
In the case we have 3 entities linked together likes:
There is listener on A and C to generate tha A, B and C references. The A listener loop over the collection of B. It works well in the application, but when I want to create a fixture for A... It totally explode, when Foundry call the persist, there is no B inside the collection (in A class). But to generate the A reference I need the first B class of the collection and the linked C class.
Someone know how we can do this ? Actually I've something similar to:
AFactory::createOne();
Internally:
The text was updated successfully, but these errors were encountered: