diff --git a/.env b/.env
index 03264939b..4033ed180 100644
--- a/.env
+++ b/.env
@@ -11,3 +11,6 @@ USE_DAMA_DOCTRINE_TEST_BUNDLE="0"
USE_FOUNDRY_PHPUNIT_EXTENSION="0"
USE_PHP_84_LAZY_OBJECTS="0"
PHPUNIT_VERSION="12" # allowed values: 9, 10, 11, 12
+
+# Only relevant for "reset-database" testsuite
+DATABASE_RESET_MODE="schema" # allowed values: schema, migrate
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index adc87dca8..ca1f237c3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -133,6 +133,7 @@ jobs:
DATABASE_RESET_MODE: ${{ matrix.reset-database-mode == 1 && 1 || 0 }}
MIGRATION_CONFIGURATION_FILE: ${{ matrix.migration-configuration-file == 'no' && '' || format('tests/Fixture/MigrationTests/configs/{0}.php', matrix.migration-configuration-file) }}
PHPUNIT_VERSION: 11
+ USE_FOUNDRY_PHPUNIT_EXTENSION: 1
services:
postgres:
image: ${{ contains(matrix.database, 'pgsql') && 'postgres:15' || '' }}
@@ -189,7 +190,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/README.md b/README.md
index e66a91662..434e5b137 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,7 @@ $ composer update
# run main testsuite (with "schema" reset database strategy)
$ ./phpunit
-# run "migrate" testsuite (with "migrate" reset database strategy)
+# run "reset-database" testsuite
$ ./phpunit --testsuite reset-database
```
@@ -73,6 +73,7 @@ PHPUNIT_VERSION="11" # possible values: 9, 10, 11, 11.4
# test reset database with migrations,
# only relevant for "reset-database" testsuite
+DATABASE_RESET_MODE="migrate"
MIGRATION_CONFIGURATION_FILE="tests/Fixture/MigrationTests/configs/migration-configuration.php"
# run test suite with postgreSQL
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.8.md b/UPGRADE-2.8.md
new file mode 100644
index 000000000..cca974d71
--- /dev/null
+++ b/UPGRADE-2.8.md
@@ -0,0 +1,53 @@
+# Migration guide from Foundry 2.7 to 2.8
+
+The main feature of Foundry 2.8 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_8])
+;
+```
diff --git a/UPGRADE-2.9.md b/UPGRADE-2.9.md
new file mode 100644
index 000000000..58e408b98
--- /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 `ResetDatabase` trait, in favor of a `#[ResetDatabase]` attribute,
+along with the [PHPUnit extension](https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#phpunit-extension)
+shipped by Foundry.
+
+The trait will be removed in Foundry 3.0, and the usage of the attribute will be mandatory to reset the database in your tests.
+
+> [!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, replace all the `use ResetDatabase;` statements by a `#[\Zenstruck\Foundry\Attribute\ResetDatabase]` attribute
+on your test classes. Note that you can put the attribute on a parent class, it will be inherited by all its children.
+
+## Rector rules
+
+A Rector set is available to automatically replace the trait by the attribute 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 1355a89c6..4c4ff6d03 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -1606,59 +1606,90 @@ 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.8
+
+ The ability to globally enable Foundry with PHPUnit extension was introduced in Foundry 2.8 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;
- public function test_1(): void
+ class MyTest extends WebTestCase
{
- $post = PostFactory::createOne();
+ use Factories;
- // ...
+ public function test_something(): void
+ {
+ $post = PostFactory::createOne();
+
+ // ...
+ }
}
- }
Database Reset
~~~~~~~~~~~~~~
-This library requires that your database be reset before each test. The packaged ``ResetDatabase`` trait handles
+This library requires that your database be reset before each test. The packaged ``ResetDatabase`` attribute handles
this for you.
::
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
- use Zenstruck\Foundry\Test\Factories;
- use Zenstruck\Foundry\Test\ResetDatabase;
+ use Zenstruck\Foundry\Attribute\ResetDatabase;
+ #{ResetDatabase]
class MyTest extends WebTestCase
{
- use ResetDatabase, Factories;
-
// ...
}
-Before the first test using the ``ResetDatabase`` trait, it drops (if exists) and creates the test database.
+Before the first test using the ``ResetDatabase`` attribute, it drops (if exists) and creates the test database.
Then, by default, before each test, it resets the schema using ``doctrine:schema:drop``/``doctrine:schema:create``.
+.. note::
+
+ If you're still using PHPUnit 9, the database can be reset by adding the trait ``Zenstruck\Foundry\Test\ResetDatabase``::
+
+ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+ use Zenstruck\Foundry\Test\Factories;
+ use Zenstruck\Foundry\Test\ResetDatabase;
+
+ class MyTest extends WebTestCase
+ {
+ use ResetDatabase, Factories;
+
+ // ...
+ }
+
.. tip::
- Create a base TestCase for tests using factories to avoid adding the traits to every TestCase.
+ Create a base TestCase for tests using factories to avoid adding the attribute to every TestCase.
.. tip::
If your tests :ref:`are not persisting ` the objects they create, the ``ResetDatabase``
- trait is not required.
+ attribute is not required.
By default, ``ResetDatabase`` resets the default configured connection's database and default configured object manager's
schema. To customize the connection's and object manager's to be reset (or reset multiple connections/managers), use the
@@ -1780,7 +1811,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
{
@@ -2030,7 +2061,7 @@ Global State
If you have an initial database state you want for all tests, you can set this in the config of the bundle. Accepted
values are: stories as service, "global" stories and invokable services. Global state is loaded before each test using
-the ``ResetDatabase`` trait. If you are using `DamaDoctrineTestBundle`_, it is only loaded once for the entire
+the ``ResetDatabase`` attribute. If you are using `DamaDoctrineTestBundle`_, it is only loaded once for the entire
test suite.
.. configuration-block::
@@ -2053,7 +2084,7 @@ test suite.
.. note::
- The :ref:`ResetDatabase ` trait is required when using global state.
+ The :ref:`ResetDatabase ` attribute is required when using global state.
.. warning::
@@ -2249,7 +2280,7 @@ This library integrates seamlessly with `DAMADoctrineTestBundle withoutPersisting()`` is not necessary). Because the bundle is not available in these tests,
-any bundle configuration you have will not be picked up.
+``Symfony\Bundle\FrameworkBundle\Test\KernelTestCase``). These tests still require enabling Foundry with the PHPUnit extension
+(or using the ``Factories`` trait if you still use PHPUnit 9) to boot Foundry but will not have doctrine available.
+Factories created in these tests will not be persisted (calling ``->withoutPersisting()`` is not necessary). Because
+the bundle is not available in these tests, any bundle configuration you have will not be picked up.
::
@@ -2378,8 +2409,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();
@@ -2596,19 +2625,19 @@ Full Default Bundle Configuration
orm:
reset:
- # DBAL connections to reset with ResetDatabase trait
+ # DBAL connections to reset with ResetDatabase attribute
connections:
# Default:
- default
- # Entity Managers to reset with ResetDatabase trait
+ # Entity Managers to reset with ResetDatabase attribute
entity_managers:
# Default:
- default
- # Reset mode to use with ResetDatabase trait
+ # Reset mode to use with ResetDatabase attribute
mode: schema # One of "schema"; "migrate"
migrations:
@@ -2618,7 +2647,7 @@ Full Default Bundle Configuration
mongo:
reset:
- # Document Managers to reset with ResetDatabase trait
+ # Document Managers to reset with ResetDatabase attribute
document_managers:
# Default:
diff --git a/phpstan.neon b/phpstan.neon
index c79993e27..4d6dc0903 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 56e3635af..5448ab66e 100644
--- a/phpunit-deprecation-baseline.xml
+++ b/phpunit-deprecation-baseline.xml
@@ -7,13 +7,20 @@
+
+
+
-
+
+
diff --git a/src/Attribute/ResetDatabase.php b/src/Attribute/ResetDatabase.php
new file mode 100644
index 000000000..ccde895c8
--- /dev/null
+++ b/src/Attribute/ResetDatabase.php
@@ -0,0 +1,19 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\Attribute;
+
+#[\Attribute(\Attribute::TARGET_CLASS)]
+final class ResetDatabase
+{
+}
diff --git a/src/Configuration.php b/src/Configuration.php
index d531f27cd..55c3f4267 100644
--- a/src/Configuration.php
+++ b/src/Configuration.php
@@ -20,6 +20,7 @@
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
use Zenstruck\Foundry\Persistence\PersistenceManager;
use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker;
+use Zenstruck\Foundry\PHPUnit\FoundryExtension;
/**
* @author Kevin Bond
@@ -117,7 +118,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;
}
@@ -132,6 +135,10 @@ public static function boot(\Closure|self $configuration): void
{
PersistedObjectsTracker::reset();
self::$instance = $configuration;
+
+ if (FoundryExtension::shouldBeEnabled()) {
+ trigger_deprecation('zenstruck/foundry', '2.8', '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.8.md to upgrade.');
+ }
}
/** @param \Closure():self|self $configuration */
diff --git a/src/Exception/FoundryNotBooted.php b/src/Exception/FoundryNotBooted.php
index 5f7fbef27..d6fa3fc8c 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::isEnabled()
+ ? '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/PHPUnit/AttributeReader.php b/src/PHPUnit/AttributeReader.php
new file mode 100644
index 000000000..34da8c67f
--- /dev/null
+++ b/src/PHPUnit/AttributeReader.php
@@ -0,0 +1,42 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class AttributeReader
+{
+ private function __construct()
+ {
+ }
+
+ /**
+ * @template T of object
+ *
+ * @param class-string $attributeClass
+ *
+ * @return list<\ReflectionAttribute>
+ */
+ public static function collectAttributesFromClassAndParents(string $attributeClass, \ReflectionClass $class): array // @phpstan-ignore missingType.generics
+ {
+ return [
+ ...$class->getAttributes($attributeClass),
+ ...(
+ $class->getParentClass()
+ ? self::collectAttributesFromClassAndParents($attributeClass, $class->getParentClass())
+ : []
+ ),
+ ];
+ }
+}
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..aaea4a092
--- /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::getContainer($className)->has('.zenstruck_foundry.configuration')) {
+ throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.');
+ }
+
+ return KernelTestCaseHelper::getContainer($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type
+ });
+ }
+}
diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php
index ff3ea9eb4..e68c6dc5d 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
@@ -35,7 +34,7 @@ public function notify(Event\Test\Prepared $event): void
/** @var Event\Code\TestMethod $test */
$reflectionClass = new \ReflectionClass($test->className());
$withStoryAttributes = [
- ...$this->collectWithStoryAttributesFromClassAndParents($reflectionClass),
+ ...AttributeReader::collectAttributesFromClassAndParents(WithStory::class, $reflectionClass),
...$reflectionClass->getMethod($test->methodName())->getAttributes(WithStory::class),
];
@@ -47,25 +46,8 @@ 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();
}
}
-
- /**
- * @return list<\ReflectionAttribute>
- */
- private function collectWithStoryAttributesFromClassAndParents(\ReflectionClass $class): array // @phpstan-ignore missingType.generics
- {
- return [
- ...$class->getAttributes(WithStory::class),
- ...(
- $class->getParentClass()
- ? $this->collectWithStoryAttributesFromClassAndParents($class->getParentClass())
- : []
- ),
- ];
- }
}
diff --git a/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php
new file mode 100644
index 000000000..c94209151
--- /dev/null
+++ b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php
@@ -0,0 +1,73 @@
+
+ *
+ * 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\Persistence\PersistentObjectFromDataProviderRegistry;
+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());
+
+ PersistentObjectFromDataProviderRegistry::instance()->addDataset(
+ $event->testMethod()->className(),
+ $event->testMethod()->methodName(),
+ "{$event->dataProviderMethod()->className()}::{$event->dataProviderMethod()->methodName()}"(...) // @phpstan-ignore callable.nonCallable
+ );
+
+ $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::getContainer($className)->has('.zenstruck_foundry.configuration')) {
+ throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.');
+ }
+
+ return KernelTestCaseHelper::getContainer($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type
+ });
+ }
+}
diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php
similarity index 69%
rename from src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php
rename to src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php
index b028394b3..690ecfa7f 100644
--- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php
+++ b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php
@@ -11,9 +11,11 @@
* file that was distributed with this source code.
*/
-namespace Zenstruck\Foundry\PHPUnit;
+namespace Zenstruck\Foundry\PHPUnit\DataProvider;
use PHPUnit\Event;
+use Zenstruck\Foundry\Configuration;
+use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
/**
* @internal
@@ -23,8 +25,8 @@ final class ShutdownFoundryOnDataProviderMethodFinished implements Event\Test\Da
{
public function notify(Event\Test\DataProviderMethodFinished $event): void
{
- if (\method_exists($event->testMethod()->className(), '_shutdownAfterDataProvider')) {
- $event->testMethod()->className()::_shutdownAfterDataProvider();
- }
+ 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..50fe7ecfd
--- /dev/null
+++ b/src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php
@@ -0,0 +1,44 @@
+
+ *
+ * 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..d8e6c79c8 100644
--- a/src/PHPUnit/FoundryExtension.php
+++ b/src/PHPUnit/FoundryExtension.php
@@ -17,35 +17,79 @@
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;
+use Zenstruck\Foundry\PHPUnit\ResetDatabase\ResetDatabaseBeforeEachTest;
+use Zenstruck\Foundry\PHPUnit\ResetDatabase\ResetDatabaseBeforeFirstTest;
/**
* @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();
+ }
+
+ 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(),
+ new ShutdownFoundryOnDataProviderMethodFinished(),
+
+ // must be added BEFORE ResetDatabaseBeforeEachTest
+ new TriggerDataProviderPersistenceOnTestPrepared(),
+ ];
+ }
+
+ $subscribers = [
+ ...($subscribers ?? []),
+ new BuildStoryOnTestPrepared(),
+ new EnableInMemoryBeforeTest(),
+ new DisplayFakerSeedOnTestSuiteFinished(),
+ new BootFoundryOnPreparationStarted(),
+ new ShutdownFoundryOnTestFinished(),
+ new ResetDatabaseBeforeFirstTest(),
+ new ResetDatabaseBeforeEachTest(),
+ ];
+
+ $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
+ {
+ return false;
}
- $facade->registerSubscribers(...$subscribers);
+ public static function isEnabled(): bool
+ {
+ return false;
+ }
}
}
diff --git a/src/PHPUnit/KernelTestCaseHelper.php b/src/PHPUnit/KernelTestCaseHelper.php
new file mode 100644
index 000000000..0b1e9615a
--- /dev/null
+++ b/src/PHPUnit/KernelTestCaseHelper.php
@@ -0,0 +1,87 @@
+
+ *
+ * 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;
+use Symfony\Component\HttpKernel\KernelInterface;
+
+/**
+ * @internal
+ */
+final class KernelTestCaseHelper
+{
+ /**
+ * @param class-string $class
+ */
+ public static function getContainer(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,
+ ))();
+ }
+
+ /**
+ * @param class-string $class
+ */
+ public static function bootKernel(string $class): KernelInterface
+ {
+ if (!\is_subclass_of($class, KernelTestCase::class)) {
+ throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, KernelTestCase::class));
+ }
+
+ return (\Closure::bind(
+ fn() => $class::bootKernel(),
+ newThis: null,
+ newScope: $class,
+ ))();
+ }
+
+ /**
+ * @param class-string $class
+ */
+ public static function ensureKernelShutdown(string $class): void
+ {
+ if (!\is_subclass_of($class, KernelTestCase::class)) {
+ throw new \LogicException(\sprintf('Class "%s" must extend "%s".', $class, KernelTestCase::class));
+ }
+
+ (\Closure::bind(
+ fn() => $class::ensureKernelShutdown(),
+ newThis: null,
+ newScope: $class,
+ ))();
+ }
+}
diff --git a/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php
new file mode 100644
index 000000000..f088004bd
--- /dev/null
+++ b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php
@@ -0,0 +1,49 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit\ResetDatabase;
+
+use PHPUnit\Event;
+use PHPUnit\Event\Test\Prepared;
+use Zenstruck\Foundry\Attribute\ResetDatabase;
+use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
+use Zenstruck\Foundry\PHPUnit\AttributeReader;
+use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class ResetDatabaseBeforeEachTest implements Event\Test\PreparedSubscriber
+{
+ public function notify(Prepared $event): void
+ {
+ $test = $event->test();
+
+ if (!$test->isTestMethod()) {
+ return;
+ }
+ /** @var Event\Code\TestMethod $test */
+ $resetDatabaseAttributes = AttributeReader::collectAttributesFromClassAndParents(
+ ResetDatabase::class,
+ new \ReflectionClass($test->className())
+ );
+
+ if ([] === $resetDatabaseAttributes) {
+ return;
+ }
+
+ ResetDatabaseManager::resetBeforeEachTest(
+ static fn() => KernelTestCaseHelper::bootKernel($test->className()),
+ static fn() => KernelTestCaseHelper::ensureKernelShutdown($test->className()),
+ );
+ }
+}
diff --git a/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php
new file mode 100644
index 000000000..a70654ffe
--- /dev/null
+++ b/src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php
@@ -0,0 +1,53 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\PHPUnit\ResetDatabase;
+
+use PHPUnit\Event;
+use PHPUnit\Event\TestSuite\Started as TestSuiteStarted;
+use Zenstruck\Foundry\Attribute\ResetDatabase;
+use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
+use Zenstruck\Foundry\PHPUnit\AttributeReader;
+use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class ResetDatabaseBeforeFirstTest implements Event\TestSuite\StartedSubscriber
+{
+ public function notify(TestSuiteStarted $event): void
+ {
+ if (!$event->testSuite()->isForTestClass()) {
+ return;
+ }
+
+ $testClassName = $event->testSuite()->name();
+
+ if (!\class_exists($testClassName)) {
+ return;
+ }
+
+ $resetDatabaseAttributes = AttributeReader::collectAttributesFromClassAndParents(
+ ResetDatabase::class,
+ new \ReflectionClass($testClassName)
+ );
+
+ if ([] === $resetDatabaseAttributes) {
+ return;
+ }
+
+ ResetDatabaseManager::resetBeforeFirstTest(
+ static fn() => KernelTestCaseHelper::bootKernel($testClassName),
+ static fn() => KernelTestCaseHelper::ensureKernelShutdown($testClassName),
+ );
+ }
+}
diff --git a/src/PHPUnit/ShutdownFoundryOnTestFinished.php b/src/PHPUnit/ShutdownFoundryOnTestFinished.php
new file mode 100644
index 000000000..bfa9b99fb
--- /dev/null
+++ b/src/PHPUnit/ShutdownFoundryOnTestFinished.php
@@ -0,0 +1,29 @@
+
+ *
+ * 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;
+
+/**
+ * @internal
+ * @author Nicolas PHILIPPE
+ */
+final class ShutdownFoundryOnTestFinished implements Event\Test\FinishedSubscriber
+{
+ public function notify(Event\Test\Finished $event): void
+ {
+ Configuration::shutdown();
+ }
+}
diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php
index 411e5a268..58e7ee697 100644
--- a/src/Persistence/PersistentObjectFactory.php
+++ b/src/Persistence/PersistentObjectFactory.php
@@ -245,7 +245,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..740e21e4d
--- /dev/null
+++ b/src/Persistence/PersistentObjectFromDataProviderRegistry.php
@@ -0,0 +1,111 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Zenstruck\Foundry\Persistence;
+
+/**
+ * If a persistent object has been created in a data provider, we need to initialize the proxy object,
+ * which will trigger the object to be persisted.
+ *
+ * Otherwise, such 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!
+ * We collect all the "datasets" and we trigger the persistence for each one before the test is executed.
+ * This means that de data providers 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