diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index 5ba91e373..9c724cb41 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -202,7 +202,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- php: [ 8.1, 8.2, 8.3, 8.4 ]
+ php: [ 8.2, 8.3, 8.4 ]
steps:
- name: Checkout code
uses: actions/checkout@v3
diff --git a/UPGRADE-2.7.md b/UPGRADE-2.7.md
index 88bfc072a..2a5937c55 100644
--- a/UPGRADE-2.7.md
+++ b/UPGRADE-2.7.md
@@ -52,7 +52,7 @@ return RectorConfig::configure()
'src',
'tests'
])
- ->withSets([FoundrySetList::REMOVE_PROXIES])
+ ->withSets([FoundrySetList::FOUNDRY_2_7])
;
```
diff --git a/UPGRADE-2.9.md b/UPGRADE-2.9.md
new file mode 100644
index 000000000..94fdae932
--- /dev/null
+++ b/UPGRADE-2.9.md
@@ -0,0 +1,53 @@
+# Migration guide from Foundry 2.8 to 2.9
+
+The main feature of Foundry 2.9 is the deprecation of the `Factories` trait, in favor of the [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension)
+shipped by Foundry. It was necessary to remember to add the trait in every test class. And in some cases, Foundry could
+still work even if the trait wasn’t added to the test, which could lead to subtle bugs. Now, Foundry is globally enabled
+once for all.
+
+The trait will be removed in Foundry 3.0, and the extension will be mandatory.
+
+> [!WARNING]
+> The PHPUnit extension mechanism was introduced in PHPUnit 10. This means that Foundry 3 won't be compatible
+> with PHPUnit 9 anymore (but Foundry 2 will remain compatible with PHPUnit 9).
+
+## How to
+
+> [!IMPORTANT]
+> If you're still not using PHPUnit 10 or grater, there is nothing to do (yet!)
+
+Enable Foundry's [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension)
+in your `phpunit.xml` file:
+
+```xml
+
+
+
+
+
+```
+
+And then, remove all the `use Factories;` statements from your factories.
+
+## Rector rules
+
+A Rector set is available to automatically remove the usage of the trait in all your tests.
+
+First, you'll need to install `rector/rector`:
+```shell
+composer require --dev rector/rector
+```
+
+Then, create a `rector.php` file:
+
+```php
+withPaths(['tests'])
+ ->withSets([FoundrySetList::FOUNDRY_2_9])
+;
+```
diff --git a/docs/index.rst b/docs/index.rst
index 9897437f4..acc60d6fd 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1683,28 +1683,46 @@ Let's look at an example:
.. _enable-foundry-in-your-testcase:
-Enable Foundry in your TestCase
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Globally Enable Foundry In PHPUnit
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Add the ``Factories`` trait for tests using factories:
+Add Foundry's `PHPUnit Extension`_ in your `phpunit.xml` file:
-::
+.. configuration-block::
- use App\Factory\PostFactory;
- use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
- use Zenstruck\Foundry\Test\Factories;
+ .. code-block:: xml
- class MyTest extends WebTestCase
- {
- use Factories;
+
+
+
+
+
+
+.. versionadded:: 2.9
- public function test_1(): void
+ The ability to globally enable Foundry with PHPUnit extension was introduced in Foundry 2.9 and requires at least
+ PHPUnit 10.
+
+.. note::
+
+ If you're still using PHPUnit 9, Foundry can be enabled by adding the trait ``Zenstruck\Foundry\Test\Factories``
+ in each test::
+
+ use App\Factory\PostFactory;
+ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+ use Zenstruck\Foundry\Test\Factories;
+
+ class MyTest extends WebTestCase
{
- $post = PostFactory::createOne();
+ use Factories;
- // ...
+ public function test_something(): void
+ {
+ $post = PostFactory::createOne();
+
+ // ...
+ }
}
- }
Database Reset
~~~~~~~~~~~~~~
@@ -1857,7 +1875,7 @@ Foundry provides a mechanism to automatically refresh inside a functional test t
class MyTest extends WebTestCase
{
- use Factories, ResetDatabase;
+ use ResetDatabase;
public function test_with_autorefresh(): void
{
@@ -2455,8 +2473,6 @@ any bundle configuration you have will not be picked up.
class MyUnitTest extends TestCase
{
- use Factories;
-
public function some_test(): void
{
$post = PostFactory::createOne();
diff --git a/phpstan.neon b/phpstan.neon
index 9ac64d6fa..124e3db45 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -30,10 +30,6 @@ parameters:
- identifier: missingType.iterableValue
path: tests/
- # We support both PHPUnit versions (this method changed in PHPUnit 10)
- - identifier: function.impossibleType
- path: src/Test/Factories.php
-
# PHPStan does not understand PHP version checks
- message: '#Comparison operation "(<|>|<=|>=)" between int<80\d+, 80\d+> and 80\d+ is always (false|true).#'
diff --git a/phpunit-deprecation-baseline.xml b/phpunit-deprecation-baseline.xml
index 4b2706232..843cddece 100644
--- a/phpunit-deprecation-baseline.xml
+++ b/phpunit-deprecation-baseline.xml
@@ -13,7 +13,9 @@
Foundry now leverages the native PHP lazy system to auto-refresh objects (it can be enabled with "zenstruck_foundry.enable_auto_refresh_with_lazy_objects" configuration).
See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.7.md to upgrade.]]>
-
+
+
+
diff --git a/src/Configuration.php b/src/Configuration.php
index b21b6836a..76dab41a0 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -21,6 +21,7 @@
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
use Zenstruck\Foundry\Persistence\PersistedObjectsTracker;
use Zenstruck\Foundry\Persistence\PersistenceManager;
+use Zenstruck\Foundry\PHPUnit\FoundryExtension;
/**
* @author Kevin Bond
@@ -129,7 +130,9 @@ public static function instance(): self
throw new FoundryNotBooted();
}
- FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait();
+ if (!FoundryExtension::isEnabled()) {
+ FactoriesTraitNotUsed::throwIfComingFromKernelTestCaseWithoutFactoriesTrait();
+ }
return \is_callable(self::$instance) ? (self::$instance)() : self::$instance;
}
@@ -143,6 +146,10 @@ public static function isBooted(): bool
public static function boot(\Closure|self $configuration): void
{
self::$instance = $configuration;
+
+ if (FoundryExtension::shouldBeEnabled()) {
+ trigger_deprecation('zenstruck/foundry', '2.9', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.9.md to upgrade.');
+ }
}
/** @param \Closure():self|self $configuration */
diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php
index 5f7fbef27..353058bae 100644
--- a/src/Exception/FoundryNotBooted.php
+++ b/src/Exception/FoundryNotBooted.php
@@ -11,6 +11,8 @@
namespace Zenstruck\Foundry\Exception;
+use Zenstruck\Foundry\PHPUnit\FoundryExtension;
+
/**
* @author Kevin Bond
*/
@@ -18,6 +20,10 @@ final class FoundryNotBooted extends \LogicException
{
public function __construct()
{
- parent::__construct('Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.');
+ $message = FoundryExtension::shouldBeEnabled()
+ ? 'Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure Foundry\'s PHPUnit extension is enabled.'
+ : 'Foundry is not yet booted. Ensure ZenstruckFoundryBundle is enabled. If in a test, ensure your TestCase has the Factories trait.';
+
+ parent::__construct($message);
}
}
diff --git a/src/FactoryCollection.php b/src/FactoryCollection.php
index e6a1e47d4..8da7d9d5c 100644
--- a/src/FactoryCollection.php
+++ b/src/FactoryCollection.php
@@ -125,6 +125,10 @@ public static function range(Factory $factory, int $min, int $max): self
throw new \InvalidArgumentException('Min must be less than max.');
}
+ if ($factory instanceof PersistentObjectFactory && $factory->isPersisting() && Configuration::instance()->inADataProvider()) {
+ throw new \InvalidArgumentException('Using randomized "range" factory in data provider is not supported.');
+ }
+
return new self($factory, static fn() => \array_fill(0, \mt_rand($min, $max), []));
}
diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php
deleted file mode 100644
index 8f58ca6bb..000000000
--- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php
+++ /dev/null
@@ -1,38 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Zenstruck\Foundry\PHPUnit;
-
-use PHPUnit\Event;
-use Zenstruck\Foundry\Configuration;
-use Zenstruck\Foundry\InMemory\AsInMemoryTest;
-
-/**
- * @internal
- * @author Nicolas PHILIPPE
- */
-final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber
-{
- public function notify(Event\Test\DataProviderMethodCalled $event): void
- {
- if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) {
- $event->testMethod()->className()::_bootForDataProvider();
- }
-
- $testMethod = $event->testMethod();
-
- if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) {
- Configuration::instance()->enableInMemory();
- }
- }
-}
diff --git a/src/PHPUnit/BootFoundryOnPreparationStarted.php b/src/PHPUnit/BootFoundryOnPreparationStarted.php
new file mode 100644
index 000000000..f22316d7c
--- /dev/null
+++ b/src/PHPUnit/BootFoundryOnPreparationStarted.php
@@ -0,0 +1,64 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit;
+
+use PHPUnit\Event;
+use PHPUnit\Framework\TestCase;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Zenstruck\Foundry\Configuration;
+use Zenstruck\Foundry\Test\UnitTestConfig;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class BootFoundryOnPreparationStarted implements Event\Test\PreparationStartedSubscriber
+{
+ public function notify(Event\Test\PreparationStarted $event): void
+ {
+ $test = $event->test();
+
+ if (!$test->isTestMethod()) {
+ return;
+ }
+ /** @var Event\Code\TestMethod $test */
+ $this->bootFoundry($test->className());
+ }
+
+ /**
+ * @param class-string $className
+ */
+ private function bootFoundry(string $className): void
+ {
+ if (!\is_subclass_of($className, TestCase::class)) {
+ return;
+ }
+
+ // unit test
+ if (!\is_subclass_of($className, KernelTestCase::class)) {
+ Configuration::boot(UnitTestConfig::build());
+
+ return;
+ }
+
+ // integration test
+ Configuration::boot(static function() use ($className): Configuration {
+ if (!KernelTestCaseHelper::getContainerForTestClass($className)->has('.zenstruck_foundry.configuration')) {
+ throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.');
+ }
+
+ return KernelTestCaseHelper::getContainerForTestClass($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type
+ });
+ }
+}
diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php
index ff3ea9eb4..8689dbfee 100644
--- a/src/PHPUnit/BuildStoryOnTestPrepared.php
+++ b/src/PHPUnit/BuildStoryOnTestPrepared.php
@@ -16,7 +16,6 @@
use PHPUnit\Event;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Zenstruck\Foundry\Attribute\WithStory;
-use Zenstruck\Foundry\Exception\FactoriesTraitNotUsed;
/**
* @internal
@@ -47,8 +46,6 @@ public function notify(Event\Test\Prepared $event): void
throw new \InvalidArgumentException(\sprintf('The test class "%s" must extend "%s" to use the "%s" attribute.', $test->className(), KernelTestCase::class, WithStory::class));
}
- FactoriesTraitNotUsed::throwIfClassDoesNotHaveFactoriesTrait($test->className());
-
foreach ($withStoryAttributes as $withStoryAttribute) {
$withStoryAttribute->newInstance()->story::load();
}
diff --git a/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php
new file mode 100644
index 000000000..da7677ac5
--- /dev/null
+++ b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php
@@ -0,0 +1,66 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit\DataProvider;
+
+use PHPUnit\Event;
+use PHPUnit\Framework\TestCase;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Zenstruck\Foundry\Configuration;
+use Zenstruck\Foundry\InMemory\AsInMemoryTest;
+use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
+use Zenstruck\Foundry\Test\UnitTestConfig;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProviderMethodCalledSubscriber
+{
+ public function notify(Event\Test\DataProviderMethodCalled $event): void
+ {
+ $this->bootFoundryForDataProvider($event->testMethod()->className());
+
+ $testMethod = $event->testMethod();
+
+ if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) {
+ Configuration::instance()->enableInMemory();
+ }
+ }
+
+ /**
+ * @param class-string $className
+ */
+ private function bootFoundryForDataProvider(string $className): void
+ {
+ if (!\is_subclass_of($className, TestCase::class)) {
+ return;
+ }
+
+ // unit test
+ if (!\is_subclass_of($className, KernelTestCase::class)) {
+ Configuration::bootForDataProvider(UnitTestConfig::build());
+
+ return;
+ }
+
+ // integration test
+ Configuration::bootForDataProvider(static function() use ($className): Configuration {
+ if (!KernelTestCaseHelper::getContainerForTestClass($className)->has('.zenstruck_foundry.configuration')) {
+ throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.');
+ }
+
+ return KernelTestCaseHelper::getContainerForTestClass($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type
+ });
+ }
+}
diff --git a/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php
new file mode 100644
index 000000000..774448b16
--- /dev/null
+++ b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php
@@ -0,0 +1,39 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit\DataProvider;
+
+use PHPUnit\Event;
+use Zenstruck\Foundry\Configuration;
+use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry;
+use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber
+{
+ public function notify(Event\Test\DataProviderMethodFinished $event): void
+ {
+ PersistentObjectFromDataProviderRegistry::instance()->storeDatasetIfFoundryWasUsedInDataProvider(
+ $event->testMethod()->className(),
+ $event->testMethod()->methodName(),
+ ...$event->calledMethods(),
+ );
+
+ KernelTestCaseHelper::tearDownClass($event->testMethod()->className());
+
+ Configuration::shutdown();
+ }
+}
diff --git a/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php b/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php
new file mode 100644
index 000000000..81e675725
--- /dev/null
+++ b/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php
@@ -0,0 +1,43 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit\DataProvider;
+
+use PHPUnit\Event;
+use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class TriggerDataProviderPersistenceOnTestPrepared implements Event\Test\PreparedSubscriber
+{
+ public function notify(Event\Test\Prepared $event): void
+ {
+ $test = $event->test();
+
+ if (!$test->isTestMethod()) {
+ return;
+ }
+ /** @var Event\Code\TestMethod $test */
+ if (!$test->testData()->hasDataFromDataProvider() || $test->metadata()->isDataProvider()->isEmpty()) {
+ return;
+ }
+
+ PersistentObjectFromDataProviderRegistry::instance()->triggerPersistenceForDataset(
+ $test->className(),
+ $test->methodName(),
+ $test->testData()->dataFromDataProvider()->dataSetName(),
+ );
+ }
+}
diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php
index 240e8c4be..7725f3673 100644
--- a/src/PHPUnit/FoundryExtension.php
+++ b/src/PHPUnit/FoundryExtension.php
@@ -17,35 +17,70 @@
use PHPUnit\Runner;
use PHPUnit\TextUI;
use Zenstruck\Foundry\Configuration;
+use Zenstruck\Foundry\PHPUnit\DataProvider\BootFoundryOnDataProviderMethodCalled;
+use Zenstruck\Foundry\PHPUnit\DataProvider\ShutdownFoundryOnDataProviderMethodFinished;
+use Zenstruck\Foundry\PHPUnit\DataProvider\TriggerDataProviderPersistenceOnTestPrepared;
/**
* @internal
* @author Nicolas PHILIPPE
*/
-final class FoundryExtension implements Runner\Extension\Extension
-{
- public function bootstrap(
- TextUI\Configuration\Configuration $configuration,
- Runner\Extension\Facade $facade,
- Runner\Extension\ParameterCollection $parameters,
- ): void {
- // shutdown Foundry if for some reason it has been booted before
- if (Configuration::isBooted()) {
- Configuration::shutdown();
+if (\interface_exists(Runner\Extension\Extension::class)) {
+ final class FoundryExtension implements Runner\Extension\Extension
+ {
+ private static bool $enabled = false;
+
+ public function bootstrap(
+ TextUI\Configuration\Configuration $configuration,
+ Runner\Extension\Facade $facade,
+ Runner\Extension\ParameterCollection $parameters,
+ ): void {
+ // shutdown Foundry if for some reason it has been booted before
+ if (Configuration::isBooted()) {
+ Configuration::shutdown();
+ }
+
+ $subscribers = [
+ new BuildStoryOnTestPrepared(),
+ new EnableInMemoryBeforeTest(),
+ new DisplayFakerSeedOnTestSuiteFinished(),
+ new BootFoundryOnPreparationStarted(),
+ new ShutdownFoundryOnTestFinished(),
+ ];
+
+ if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) {
+ // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used
+ $subscribers[] = new BootFoundryOnDataProviderMethodCalled();
+ $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished();
+ $subscribers[] = new TriggerDataProviderPersistenceOnTestPrepared();
+ }
+
+ $facade->registerSubscribers(...$subscribers);
+
+ self::$enabled = true;
}
- $subscribers = [
- new BuildStoryOnTestPrepared(),
- new EnableInMemoryBeforeTest(),
- new DisplayFakerSeedOnTestSuiteFinished(),
- ];
+ public static function shouldBeEnabled(): bool
+ {
+ return !self::isEnabled() && ConstraintRequirement::from('>=10')->isSatisfiedBy(Runner\Version::id());
+ }
- if (ConstraintRequirement::from('>=11.4')->isSatisfiedBy(Runner\Version::id())) {
- // those deal with data provider events which can be useful only if PHPUnit >=11.4 is used
- $subscribers[] = new BootFoundryOnDataProviderMethodCalled();
- $subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished();
+ public static function isEnabled(): bool
+ {
+ return self::$enabled;
+ }
+ }
+} else {
+ final class FoundryExtension
+ {
+ public static function shouldBeEnabled(): bool // @phpstan-ignore return.tooWideBool
+ {
+ return false;
}
- $facade->registerSubscribers(...$subscribers);
+ public static function isEnabled(): bool // @phpstan-ignore return.tooWideBool
+ {
+ return false;
+ }
}
}
diff --git a/src/PHPUnit/KernelTestCaseHelper.php b/src/PHPUnit/KernelTestCaseHelper.php
new file mode 100644
index 000000000..f0485e696
--- /dev/null
+++ b/src/PHPUnit/KernelTestCaseHelper.php
@@ -0,0 +1,54 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
+use Symfony\Component\DependencyInjection\Container;
+
+/**
+ * @internal
+ */
+final class KernelTestCaseHelper
+{
+ /**
+ * @param class-string $class
+ */
+ public static function getContainerForTestClass(string $class): Container
+ {
+ if (!\is_subclass_of($class, KernelTestCase::class)) {
+ throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, KernelTestCase::class));
+ }
+
+ return (\Closure::bind(
+ fn() => $class::getContainer(),
+ newThis: null,
+ newScope: $class,
+ ))();
+ }
+
+ /**
+ * @param class-string $class
+ */
+ public static function tearDownClass(string $class): void
+ {
+ if (!\is_subclass_of($class, TestCase::class)) {
+ throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, TestCase::class));
+ }
+
+ (\Closure::bind(
+ fn() => $class::tearDownAfterClass(),
+ newThis: null,
+ newScope: $class,
+ ))();
+ }
+}
diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/ShutdownFoundryOnTestFinished.php
similarity index 52%
rename from src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php
rename to src/PHPUnit/ShutdownFoundryOnTestFinished.php
index b028394b3..bfa9b99fb 100644
--- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php
+++ b/src/PHPUnit/ShutdownFoundryOnTestFinished.php
@@ -14,17 +14,16 @@
namespace Zenstruck\Foundry\PHPUnit;
use PHPUnit\Event;
+use Zenstruck\Foundry\Configuration;
/**
* @internal
* @author Nicolas PHILIPPE
*/
-final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\DataProviderMethodFinishedSubscriber
+final class ShutdownFoundryOnTestFinished implements Event\Test\FinishedSubscriber
{
- public function notify(Event\Test\DataProviderMethodFinished $event): void
+ public function notify(Event\Test\Finished $event): void
{
- if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) {
- $event->testMethod()->className()::_shutdownAfterDataProvider();
- }
+ Configuration::shutdown();
}
}
diff --git a/src/Persistence/IsProxy.php b/src/Persistence/IsProxy.php
index be6351864..5d06eebef 100644
--- a/src/Persistence/IsProxy.php
+++ b/src/Persistence/IsProxy.php
@@ -16,6 +16,7 @@
use Zenstruck\Foundry\Configuration;
use Zenstruck\Foundry\Exception\PersistenceNotAvailable;
use Zenstruck\Foundry\Object\Hydrator;
+use Zenstruck\Foundry\Persistence\Exception\NoPersistenceStrategy;
use Zenstruck\Foundry\Persistence\Exception\ObjectNoLongerExist;
/**
@@ -152,7 +153,7 @@ private function _autoRefresh(): void
// we don't want that "transparent" calls to _refresh() to trigger a PersistenceNotAvailable exception
// or a RefreshObjectFailed exception when the object was deleted
$this->_refresh();
- } catch (PersistenceNotAvailable|ObjectNoLongerExist) {
+ } catch (PersistenceNotAvailable|ObjectNoLongerExist|NoPersistenceStrategy) {
}
}
diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php
index 8a7b8d6c2..ea5ca2a39 100644
--- a/src/Persistence/PersistentObjectFactory.php
+++ b/src/Persistence/PersistentObjectFactory.php
@@ -241,7 +241,7 @@ public function create(callable|array $attributes = []): object
&& $this->isPersisting()
&& !$this instanceof PersistentProxyObjectFactory
) {
- return ProxyGenerator::wrapFactoryNativeProxy($this, $attributes);
+ return PersistentObjectFromDataProviderRegistry::instance()->deferObjectCreation($this->with($attributes));
}
$object = parent::create($attributes);
diff --git a/src/Persistence/PersistentObjectFromDataProviderRegistry.php b/src/Persistence/PersistentObjectFromDataProviderRegistry.php
new file mode 100644
index 000000000..ed4fb2726
--- /dev/null
+++ b/src/Persistence/PersistentObjectFromDataProviderRegistry.php
@@ -0,0 +1,133 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\Persistence;
+
+use PHPUnit\Event\Code\ClassMethod;
+
+/**
+ * If a persistent object has been created in a data provider, we need to initialize the lazy object,
+ * which will trigger the object to be persisted.
+ *
+ * Otherwise, such a test would not pass:
+ * ```php
+ * #[DataProvider('provide')]
+ * public function testSomething(MyEntity $entity): void
+ * {
+ * MyEntityFactory::assert()->count(1);
+ * }
+ *
+ * public static function provide(): iterable
+ * {
+ * yield [MyEntityFactory::createOne()];
+ * }
+ * ```
+ *
+ * Sadly, this cannot be done directly a subscriber, since PHPUnit does not give access to the actual tests instances.
+ *
+ * ⚠️ This class is highly hacky!
+ *
+ * If we detect that a persisting object was created in a data provider, we collect the "datasets" of the test,
+ * and we trigger the persistence of these objects before the test is executed.
+ *
+ * This means that the data providers using Foundry are called twice.
+ * To prevent the persisted object from being different from the one returned by the data provider, we use a "buffer" so
+ * that we can return the same object for each data provider call.
+ *
+ * @internal
+ */
+final class PersistentObjectFromDataProviderRegistry
+{
+ private static ?self $instance = null;
+
+ /** @var array> */
+ private array $datasets = [];
+
+ /** @var list