Skip to content

Commit

Permalink
fix: propagte "schedule for insert" to factory collection
Browse files Browse the repository at this point in the history
  • Loading branch information
nikophil committed Jan 4, 2025
1 parent 482fcdd commit 65fc0d0
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 33 deletions.
30 changes: 29 additions & 1 deletion src/FactoryCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

namespace Zenstruck\Foundry;

use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\Persistence\PersistMode;

/**
* @author Kevin Bond <[email protected]>
*
Expand All @@ -22,12 +25,28 @@
*/
final class FactoryCollection implements \IteratorAggregate
{
private PersistMode $persistMode;

/**
* @param TFactory $factory
* @phpstan-param \Closure():iterable<Attributes>|\Closure():iterable<TFactory> $items
*/
private function __construct(public readonly Factory $factory, private \Closure $items)
{
$this->persistMode = $this->factory instanceof PersistentObjectFactory
? $this->factory->persistMode()
: PersistMode::WITHOUT_PERSISTING;
}

/**
* @internal
*/
public function withPersistMode(PersistMode $persistMode): static
{
$clone = clone $this;
$clone->persistMode = $persistMode;

return $clone;
}

/**
Expand Down Expand Up @@ -133,7 +152,16 @@ public function all(): array
$factories[] = $this->factory->with($attributesOrFactory)->with(['__index' => $i++]);
}

return $factories; // @phpstan-ignore return.type (PHPStan does not understand we have an array of factories)
return array_map( // @phpstan-ignore return.type (PHPStan does not understand we have an array of factories)
function (Factory $f) {
if ($f instanceof PersistentObjectFactory) {
return $f->withPersistMode($this->persistMode);
}

return $f;
},
$factories
);
}

public function getIterator(): \Traversable
Expand Down
2 changes: 1 addition & 1 deletion src/Persistence/PersistMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@ enum PersistMode

public function isPersisting(): bool
{
return self::PERSIST === $this;
return self::WITHOUT_PERSISTING !== $this;
}
}
6 changes: 5 additions & 1 deletion src/Persistence/PersistenceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,11 @@ public function truncate(string $class): void
*/
public function autoPersist(string $class): bool
{
return $this->strategyFor(unproxy($class))->autoPersist();
try {
return $this->strategyFor(unproxy($class))->autoPersist();
} catch (NoPersistenceStrategy) {
return false;
}
}

/**
Expand Down
63 changes: 33 additions & 30 deletions src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,6 @@ abstract class PersistentObjectFactory extends ObjectFactory
/** @var list<callable(T):void> */
private array $tempAfterInstantiate = [];

/** @var list<callable(T):void> */
private array $tempAfterPersist = [];

/**
* @phpstan-param mixed|Parameters $criteriaOrId
*
Expand Down Expand Up @@ -205,7 +202,7 @@ public function create(callable|array $attributes = []): object

$this->throwIfCannotCreateObject();

if (!$this->isPersisting()) {
if ($this->persistMode() !== PersistMode::PERSIST) {
return $object;
}

Expand All @@ -217,12 +214,6 @@ public function create(callable|array $attributes = []): object

$configuration->persistence()->save($object);

foreach ($this->tempAfterPersist as $callback) {
$callback($object);
}

$this->tempAfterPersist = [];

if ($this->afterPersist) {
$attributes = $this->normalizedParameters ?? throw new \LogicException('Factory::$normalizedParameters has not been initialized.');

Expand Down Expand Up @@ -252,6 +243,17 @@ final public function withoutPersisting(): static
return $clone;
}

/**
* @internal
*/
public function withPersistMode(PersistMode $persistMode): static
{
$clone = clone $this;
$clone->persist = $persistMode;

return $clone;
}

/**
* @phpstan-param callable(T, Parameters, static):void $callback
*/
Expand All @@ -270,11 +272,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed
}

if ($value instanceof self && isset($this->persist)) {
$value = match ($this->persist) {
PersistMode::PERSIST => $value->andPersist(),
PersistMode::WITHOUT_PERSISTING => $value->withoutPersisting(),
PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT => $value->withoutPersistingButScheduleForInsert(),
};
$value = $value->withPersistMode($this->persist);
}

if ($value instanceof self) {
Expand All @@ -288,7 +286,7 @@ protected function normalizeParameter(string $field, mixed $value): mixed

// we need to handle the circular dependency involved by inversed one-to-one relationship:
// a placeholder object is used, which will be replaced by the real object, after its instantiation
$inversedObject = $value->withoutPersistingButScheduleForInsert()
$inversedObject = $value->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)
->create([$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor()]);

// auto-refresh computes changeset and prevents the placeholder object to be cleanly
Expand Down Expand Up @@ -323,9 +321,9 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) {
$inverseField = $inverseRelationshipMetadata->inverseField;

$this->tempAfterPersist[] = static function(object $object) use ($collection, $inverseField, $pm) {
$collection->create([$inverseField => $object]);
$pm->refresh($object);
$this->tempAfterInstantiate[] = static function(object $object) use ($collection, $inverseField, $field) {
$inverseObjects = $collection->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)->create([$inverseField => $object]);
set($object, $field, unproxy($inverseObjects));
};

// creation delegated to afterPersist hook - return empty array here
Expand Down Expand Up @@ -368,19 +366,32 @@ final protected function isPersisting(): bool
return false;
}

$persistMode = $this->persist ?? ($config->persistence()->autoPersist(static::class()) ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING);
return $this->persistMode()->isPersisting();
}

return $persistMode->isPersisting();
/**
* @internal
*/
public function persistMode(): PersistMode
{
$config = Configuration::instance();

if (!$config->isPersistenceEnabled()) {
return PersistMode::WITHOUT_PERSISTING;
}

return $this->persist ?? ($config->persistence()->autoPersist(static::class()) ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING);
}

/**
* Schedule any new object for insert right after instantiation.
* @internal
*/
final protected function initializeInternal(): static
{
return $this->afterInstantiate(
static function(object $object, array $parameters, PersistentObjectFactory $factory): void {
if (!$factory->isPersisting() && (!isset($factory->persist) || PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT !== $factory->persist)) {
if (!$factory->isPersisting()) {
return;
}

Expand All @@ -389,14 +400,6 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact
);
}

private function withoutPersistingButScheduleForInsert(): static
{
$clone = clone $this;
$clone->persist = PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT;

return $clone;
}

private function throwIfCannotCreateObject(): void
{
$configuration = Configuration::instance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public static function provideCascadeRelationshipsCombinations(): iterable
} catch (MappingException) {
throw new \LogicException(\sprintf("Wrong parameters for attribute \"%s\". Association \"{$class}::\${$field}\" does not exist.", UsingRelationships::class));
}

dump($association);die;

$relationshipFields[] = ['class' => $association['sourceEntity'], 'field' => $association['fieldName']];
if ($association['inversedBy'] ?? $association['mappedBy'] ?? null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany;

use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixture\Model\Base;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('inversed_one_to_one_with_one_to_many_inverse_side')]
class InverseSide extends Base
{
#[ORM\OneToOne(mappedBy: 'inverseSide')]
private ?OwningSide $owningSide = null;

public function getOwningSide(): ?OwningSide
{
return $this->owningSide;
}

public function setOwningSide(OwningSide $owningSide): void
{
$this->owningSide = $owningSide;
$owningSide->inverseSide = $this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany;

use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixture\Model\Base;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('inversed_one_to_one_with_one_to_many_item_if_collection')]
class Item extends Base
{
#[ORM\ManyToOne(inversedBy: 'items')]
public ?OwningSide $owningSide = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

/*
* This file is part of the zenstruck/foundry package.
*
* (c) Kevin Bond <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\InversedOneToOneWithOneToMany;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Zenstruck\Foundry\Tests\Fixture\Model\Base;

/**
* @author Nicolas PHILIPPE <[email protected]>
*/
#[ORM\Entity]
#[ORM\Table('inversed_one_to_one_with_one_to_many_owning_side')]
class OwningSide extends Base
{
#[ORM\OneToOne(inversedBy: 'owningSide')]
public ?InverseSide $inverseSide = null;

/** @var Collection<int, Item> */
#[ORM\OneToMany(targetEntity: Item::class, mappedBy: 'owningSide')]
private Collection $items;

public function __construct()
{
$this->items = new ArrayCollection();
}

/**
* @return Collection<int, Item>
*/
public function getItems(): Collection
{
return $this->items;
}

public function addItem(Item $item): void
{
if (!$this->items->contains($item)) {
$this->items->add($item);
$item->owningSide = $this;
}
}

public function removeItem(Item $item): void
{
if ($this->items->contains($item)) {
$this->items->removeElement($item);
$item->owningSide = null;
}
}
}
Loading

0 comments on commit 65fc0d0

Please sign in to comment.