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

Merge 2.3.x in 2.x #845

Merged
merged 10 commits into from
Mar 10, 2025
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## [v2.3.6](https://github.com/zenstruck/foundry/releases/tag/v2.3.6)

February 25th, 2025 - [v2.3.5...v2.3.6](https://github.com/zenstruck/foundry/compare/v2.3.5...v2.3.6)

* 300645b fix: can call ->create() in after persist callback (#833) by @nikophil

## [v2.3.5](https://github.com/zenstruck/foundry/releases/tag/v2.3.5)

February 24th, 2025 - [v2.3.4...v2.3.5](https://github.com/zenstruck/foundry/compare/v2.3.4...v2.3.5)
Expand Down
2 changes: 1 addition & 1 deletion phpunit
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ SHOULD_UPDATE_PHPUNIT=$(check_phpunit_version "${PHPUNIT_VERSION}")

if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then
echo "ℹ️ Upgrading PHPUnit to ${PHPUNIT_VERSION}"
composer update brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W
composer update dama/doctrine-test-bundle brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W
fi
### <<

Expand Down
4 changes: 2 additions & 2 deletions src/ObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public function __construct()
{
parent::__construct();

$this->validationEnabled = Configuration::instance()->validationEnabled;
$this->validationEnabled = Configuration::isBooted() && Configuration::instance()->validationEnabled;
}

/**
Expand Down Expand Up @@ -194,7 +194,7 @@ public function getValidationGroups(): string|GroupSequence|array|null
*/
protected function initializeInternal(): static
{
if (!Configuration::instance()->hasEventDispatcher()) {
if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) {
return $this;
}

Expand Down
58 changes: 32 additions & 26 deletions src/Persistence/PersistentObjectFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Doctrine\Persistence\ObjectRepository;
use Symfony\Component\VarExporter\Exception\LogicException as VarExportLogicException;
use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\Exception\FoundryNotBooted;
use Zenstruck\Foundry\Exception\PersistenceDisabled;
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
use Zenstruck\Foundry\Factory;
Expand Down Expand Up @@ -48,7 +49,7 @@ public function __construct()
{
parent::__construct();

$this->persist = Configuration::instance()->isPersistenceEnabled() ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING;
$this->persist = $this->isPersistenceEnabled() ? PersistMode::PERSIST : PersistMode::WITHOUT_PERSISTING;
}

/**
Expand Down Expand Up @@ -271,17 +272,11 @@ final public function afterPersist(callable $callback): static
*/
public function persistMode(): PersistMode
{
return Configuration::instance()->isPersistenceEnabled() ? $this->persist : PersistMode::WITHOUT_PERSISTING;
return $this->isPersistenceEnabled() ? $this->persist : PersistMode::WITHOUT_PERSISTING;
}

final public function isPersisting(): bool
{
$config = Configuration::instance();

if (!$config->isPersistenceEnabled()) {
return false;
}

return $this->persistMode()->isPersisting();
}

Expand All @@ -304,16 +299,17 @@ protected function normalizeParameter(string $field, mixed $value): mixed
if ($inversedRelationshipMetadata && !$inversedRelationshipMetadata->isCollection) {
$inverseField = $inversedRelationshipMetadata->inverseField;

// 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->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)
->create([$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor()]);
$inversedObject = $value->withPersistMode(
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
)

// auto-refresh computes changeset and prevents the placeholder object to be cleanly
// forgotten fom the persistence manager
if ($inversedObject instanceof Proxy) {
$inversedObject = $inversedObject->_real(withAutoRefresh: false);
}
// 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
->create([
$inverseField => $placeholder = (new \ReflectionClass(static::class()))->newInstanceWithoutConstructor(),
]);

$inversedObject = unproxy($inversedObject, withAutoRefresh: false);

$this->tempAfterInstantiate[] = static function(object $object) use ($inversedObject, $inverseField, $pm, $placeholder) {
$pm->forget($placeholder);
Expand All @@ -324,12 +320,12 @@ protected function normalizeParameter(string $field, mixed $value): mixed
}
}

return unproxy(parent::normalizeParameter($field, $value));
return unproxy(parent::normalizeParameter($field, $value), withAutoRefresh: false);
}

protected function normalizeCollection(string $field, FactoryCollection $collection): array
{
if (!$this->isPersisting() || !$collection->factory instanceof self) {
if (!Configuration::instance()->isPersistenceAvailable() || !$collection->factory instanceof self) {
return parent::normalizeCollection($field, $collection);
}

Expand All @@ -338,12 +334,15 @@ protected function normalizeCollection(string $field, FactoryCollection $collect
$inverseRelationshipMetadata = $pm->inverseRelationshipMetadata(static::class(), $collection->factory::class(), $field);

if ($inverseRelationshipMetadata && $inverseRelationshipMetadata->isCollection) {
$this->tempAfterInstantiate[] = static function(object $object) use ($collection, $inverseRelationshipMetadata, $field) {
$this->tempAfterInstantiate[] = function(object $object) use ($collection, $inverseRelationshipMetadata, $field) {
$inverseField = $inverseRelationshipMetadata->inverseField;

$inverseObjects = $collection->withPersistMode(PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT)->create([$inverseField => $object]);
$inverseObjects = $collection->withPersistMode(
$this->isPersisting() ? PersistMode::NO_PERSIST_BUT_SCHEDULE_FOR_INSERT : PersistMode::WITHOUT_PERSISTING
)
->create([$inverseField => $object]);

$inverseObjects = unproxy($inverseObjects);
$inverseObjects = unproxy($inverseObjects, withAutoRefresh: false);

// if the collection is indexed by a field, index the array
if ($inverseRelationshipMetadata->collectionIndexedBy) {
Expand Down Expand Up @@ -379,9 +378,7 @@ protected function normalizeObject(object $object): object
return $object;
}

if ($object instanceof Proxy) {
$object = $object->_real(withAutoRefresh: false);
}
$object = unproxy($object, withAutoRefresh: false);

$persistenceManager = $configuration->persistence();
if (!$persistenceManager->hasPersistenceFor($object)) {
Expand Down Expand Up @@ -421,7 +418,7 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact
}
);

if (!Configuration::instance()->hasEventDispatcher()) {
if (!Configuration::isBooted() || !Configuration::instance()->hasEventDispatcher()) {
return $factory;
}

Expand Down Expand Up @@ -456,4 +453,13 @@ private function throwIfCannotCreateObject(): void

throw new \LogicException(\sprintf('Cannot create object in a data provider for non-proxy factories. Transform your factory into a "%s", or call "create()" method in the test. See https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-data-providers', PersistentProxyObjectFactory::class));
}

private function isPersistenceEnabled(): bool
{
try {
return Configuration::instance()->isPersistenceEnabled();
} catch (FoundryNotBooted) {
return false;
}
}
}
6 changes: 3 additions & 3 deletions src/Persistence/ProxyGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,18 @@ public static function wrapFactory(PersistentProxyObjectFactory $factory, callab
*
* @return T
*/
public static function unwrap(mixed $what): mixed
public static function unwrap(mixed $what, bool $withAutoRefresh = true): mixed
{
if (\is_array($what)) {
return \array_map(self::unwrap(...), $what); // @phpstan-ignore return.type
return \array_map(static fn(mixed $w) => self::unwrap($w, $withAutoRefresh), $what); // @phpstan-ignore return.type
}

if (\is_string($what) && \is_a($what, Proxy::class, true)) {
return \get_parent_class($what) ?: throw new \LogicException('Could not unwrap proxy.'); // @phpstan-ignore return.type
}

if ($what instanceof Proxy) {
return $what->_real(); // @phpstan-ignore return.type
return $what->_real($withAutoRefresh); // @phpstan-ignore return.type
}

return $what;
Expand Down
4 changes: 2 additions & 2 deletions src/Persistence/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,9 @@ function proxy(object $object): object
*
* @return T
*/
function unproxy(mixed $what): mixed
function unproxy(mixed $what, bool $withAutoRefresh = true): mixed
{
return ProxyGenerator::unwrap($what);
return ProxyGenerator::unwrap($what, $withAutoRefresh);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPUnit\Framework\Attributes\DataProvider;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\Persistence\PersistenceManager;
use Zenstruck\Foundry\Tests\Integration\RequiresORM;

Expand Down Expand Up @@ -123,6 +124,8 @@ public static function provideCascadeRelationshipsCombinations(): iterable
}

yield from DoctrineCascadeRelationshipMetadata::allCombinations($relationshipFields);

Configuration::shutdown();
}

public static function setCurrentProvidedMethodName(string $methodName): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,11 +323,21 @@ public function disabling_persistence_cascades_to_children(): void
foreach ($contact->getTags() as $tag) {
$this->assertNull($tag->id);
}
}

/** @test */
#[Test]
#[DataProvider('provideCascadeRelationshipsCombinations')]
#[UsingRelationships(Contact::class, ['category'])]
public function disabling_persistence_cascades_to_children_one_to_many(): void
{
$category = static::categoryFactory()->withoutPersisting()->create([
'contacts' => static::contactFactory()->many(3),
]);

// ensure nothing was persisted in Doctrine by flushing
self::getContainer()->get(EntityManagerInterface::class)->flush(); // @phpstan-ignore method.notFound

static::contactFactory()::assert()->empty();
static::categoryFactory()::assert()->empty();

Expand All @@ -339,6 +349,27 @@ public function disabling_persistence_cascades_to_children(): void
}
}

/** @test */
#[Test]
#[DataProvider('provideCascadeRelationshipsCombinations')]
#[UsingRelationships(Contact::class, ['address'])]
public function disabling_persistence_cascades_to_children_inversed_one_to_one(): void
{
$address = static::addressFactory()->withoutPersisting()->create([
'contact' => static::contactFactory(),
]);

// ensure nothing was persisted in Doctrine by flushing
self::getContainer()->get(EntityManagerInterface::class)->flush(); // @phpstan-ignore method.notFound

static::contactFactory()::assert()->empty();
static::addressFactory()::assert()->empty();

$this->assertNull($address->id);
$this->assertInstanceOf(Contact::class, $address->getContact());
$this->assertNull($address->getContact()->id);
}

/** @test */
#[Test]
#[DataProvider('provideCascadeRelationshipsCombinations')]
Expand Down
30 changes: 30 additions & 0 deletions tests/Unit/Persistence/PersistentObjectFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

namespace Zenstruck\Foundry\Tests\Unit\Persistence;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Zenstruck\Foundry\FactoryCollection;
use Zenstruck\Foundry\Test\Factories;
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
Expand Down Expand Up @@ -62,4 +64,32 @@ public function random_or_create(): void

$this->assertSame('foo', $entity->getProp1());
}

/**
* @test
* @dataProvider factoryCollectionDataProvider
* @param FactoryCollection<GenericEntity, GenericEntityFactory> $collection
*/
#[Test] // @phpstan-ignore generics.notSubtype
#[DataProvider('factoryCollectionDataProvider')]
public function can_use_factory_collection_methods_in_data_providers(FactoryCollection $collection): void
{
self::assertEquals(
[
new GenericEntity('foo'),
],
$collection->create(),
);
}

public static function factoryCollectionDataProvider(): iterable
{
yield [
GenericEntityFactory::new()->sequence([
[
'prop1' => 'foo',
],
]),
];
}
}
Loading