From 859bd4fa724a7180234d974023882a9d3b1b372c Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 25 Aug 2025 19:35:54 +0200 Subject: [PATCH 1/6] feat: force PHPUnit extension usage --- phpunit-deprecation-baseline.xml | 4 +- src/Configuration.php | 6 ++ src/Exception/FoundryNotBooted.php | 8 ++- .../BootFoundryOnDataProviderMethodCalled.php | 33 ++++++++- .../BootFoundryOnPreparationStarted.php | 62 ++++++++++++++++ src/PHPUnit/FoundryExtension.php | 72 +++++++++++++------ src/PHPUnit/KernelTestCaseHelper.php | 45 ++++++++++++ ...ownFoundryOnDataProviderMethodFinished.php | 7 +- src/PHPUnit/ShutdownFoundryOnTestFinished.php | 29 ++++++++ src/Test/Factories.php | 51 ++++--------- src/Test/ResetDatabase.php | 2 +- ...DatabaseTraitTestWithoutFactoriesTest.php} | 2 +- ...php => KernelTestWithoutFactoriesTest.php} | 2 +- 13 files changed, 253 insertions(+), 70 deletions(-) create mode 100644 src/PHPUnit/BootFoundryOnPreparationStarted.php create mode 100644 src/PHPUnit/KernelTestCaseHelper.php create mode 100644 src/PHPUnit/ShutdownFoundryOnTestFinished.php rename tests/Integration/ForceFactoriesTraitUsage/{KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php => KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php} (94%) rename tests/Integration/ForceFactoriesTraitUsage/{KernelTestWithoutFactoriesTrait.php => KernelTestWithoutFactoriesTest.php} (88%) diff --git a/phpunit-deprecation-baseline.xml b/phpunit-deprecation-baseline.xml index 56e3635af..fe499d5e1 100644 --- a/phpunit-deprecation-baseline.xml +++ b/phpunit-deprecation-baseline.xml @@ -12,8 +12,8 @@ - - + + diff --git a/src/Configuration.php b/src/Configuration.php index d531f27cd..c77e9a701 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -19,6 +19,8 @@ use Zenstruck\Foundry\InMemory\CannotEnableInMemory; use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; /** @@ -132,6 +134,10 @@ public static function boot(\Closure|self $configuration): void { PersistedObjectsTracker::reset(); self::$instance = $configuration; + + if (FoundryExtension::shouldBeEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.7', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3.'); + } } /** @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/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php index 8f58ca6bb..e224c55d1 100644 --- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php +++ b/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php @@ -14,8 +14,11 @@ namespace Zenstruck\Foundry\PHPUnit; 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\Test\UnitTestConfig; /** * @internal @@ -25,9 +28,7 @@ final class BootFoundryOnDataProviderMethodCalled implements Event\Test\DataProv { public function notify(Event\Test\DataProviderMethodCalled $event): void { - if (\method_exists($event->testMethod()->className(), '_bootForDataProvider')) { - $event->testMethod()->className()::_bootForDataProvider(); - } + $this->bootFoundryForDataProvider($event->testMethod()->className()); $testMethod = $event->testMethod(); @@ -35,4 +36,30 @@ public function notify(Event\Test\DataProviderMethodCalled $event): void 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/BootFoundryOnPreparationStarted.php b/src/PHPUnit/BootFoundryOnPreparationStarted.php new file mode 100644 index 000000000..a7a773f0e --- /dev/null +++ b/src/PHPUnit/BootFoundryOnPreparationStarted.php @@ -0,0 +1,62 @@ + + * + * 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 + { + if (!$event->test()->isTestMethod()) { + return; + } + + $this->bootFoundry($event->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/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index 240e8c4be..fbf762e0c 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -22,30 +22,62 @@ * @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(); + } + + $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..12f7ff4ca --- /dev/null +++ b/src/PHPUnit/KernelTestCaseHelper.php @@ -0,0 +1,45 @@ + $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/ShutdownFoundryOnDataProviderMethodFinished.php index b028394b3..ed5f29f9e 100644 --- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php +++ b/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php @@ -14,6 +14,7 @@ namespace Zenstruck\Foundry\PHPUnit; use PHPUnit\Event; +use Zenstruck\Foundry\Configuration; /** * @internal @@ -23,8 +24,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/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/Test/Factories.php b/src/Test/Factories.php index 8bd39cf89..d04aae29f 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -16,6 +16,8 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; + use function Zenstruck\Foundry\Persistence\initialize_proxy_object; /** @@ -27,58 +29,31 @@ trait Factories * @internal * @before */ - #[Before] + #[Before(5)] public function _beforeHook(): void { - $this->_bootFoundry(); - $this->_loadDataProvidedProxies(); - } + $this->_loadDataProvidedProxies(); // todo remove - /** - * @internal - * @after - */ - #[After] - public static function _shutdownFoundry(): void - { - Configuration::shutdown(); - } - - /** - * @see \Zenstruck\Foundry\PHPUnit\BootFoundryOnDataProviderMethodCalled - * @internal - */ - public static function _bootForDataProvider(): void - { - if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType - // unit test - Configuration::bootForDataProvider(UnitTestConfig::build()); + if (FoundryExtension::isEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.7', sprintf('Trait %s is deprecated and will be removed in Foundry 3.', Factories::class)); return; } - // integration test - Configuration::bootForDataProvider(static function(): Configuration { - if (!static::getContainer()->has('.zenstruck_foundry.configuration')) { // @phpstan-ignore staticMethod.notFound - throw new \LogicException('ZenstruckFoundryBundle is not enabled. Ensure it is added to your config/bundles.php.'); - } - - return static::getContainer()->get('.zenstruck_foundry.configuration'); // @phpstan-ignore staticMethod.notFound, return.type - }); + $this->_bootFoundry(); } /** * @internal - * @see \Zenstruck\Foundry\PHPUnit\ShutdownFoundryOnDataProviderMethodFinished + * @after */ - public static function _shutdownAfterDataProvider(): void + #[After(5)] + public static function _shutdownFoundry(): void { - if (\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.impossibleType, function.alreadyNarrowedType - self::ensureKernelShutdown(); // @phpstan-ignore staticMethod.notFound - static::$class = null; // @phpstan-ignore staticProperty.notFound - static::$kernel = null; // @phpstan-ignore staticProperty.notFound - static::$booted = false; // @phpstan-ignore staticProperty.notFound + if (FoundryExtension::isEnabled()) { + return; } + Configuration::shutdown(); } diff --git a/src/Test/ResetDatabase.php b/src/Test/ResetDatabase.php index d17e8bf39..f07cf1e1b 100644 --- a/src/Test/ResetDatabase.php +++ b/src/Test/ResetDatabase.php @@ -42,7 +42,7 @@ public static function _resetDatabaseBeforeFirstTest(): void * @internal * @before */ - #[Before] + #[Before(10)] public static function _resetDatabaseBeforeEachTest(): void { if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php similarity index 94% rename from tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php rename to tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php index 889784d62..8d36fa425 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php @@ -18,7 +18,7 @@ use Zenstruck\Foundry\Test\ResetDatabase; #[RequiresPhpunit('>=11.0')] -final class KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTrait extends KernelTestCase +final class KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest extends KernelTestCase { use KernelTestCaseWithoutFactoriesTrait; use ResetDatabase; diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTrait.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php similarity index 88% rename from tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTrait.php rename to tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php index 38669bd1c..87e56c8fe 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTrait.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php @@ -17,7 +17,7 @@ use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; #[RequiresPhpunit('>=11.0')] -final class KernelTestWithoutFactoriesTrait extends KernelTestCase +final class KernelTestWithoutFactoriesTest extends KernelTestCase { use KernelTestCaseWithoutFactoriesTrait; } From 74f017ab8513ac2c67e4872fb2dabb3c221a5832 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 26 Aug 2025 09:03:13 +0200 Subject: [PATCH 2/6] feat: don't force using Factories trait when PHPUnit extension is enabled --- .github/workflows/ci.yml | 2 +- src/Configuration.php | 4 +- src/Test/Factories.php | 10 ++--- .../AnotherBaseTestCase.php | 2 + ...TestCaseWithBothTraitsInWrongOrderTest.php | 2 +- .../KernelTestCaseWithBothTraitsTest.php | 2 +- .../KernelTestCaseWithFactoriesTraitTest.php | 2 +- ...tDatabaseTraitTestWithoutFactoriesTest.php | 1 + .../KernelTestWithoutFactoriesTest.php | 2 +- .../SkipWithPHPUnitExtension.php | 21 +++++++++ .../UnitTestCaseWithFactoriesTraitTest.php | 2 +- .../UnitTestCaseWithoutFactoriesTraitTest.php | 2 + ...ernelTestCaseWithoutFactoriesTraitTest.php | 44 +++++++++++++++++++ .../UnitTestCaseWithoutFactoriesTraitTest.php | 34 ++++++++++++++ 14 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.php create mode 100644 tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d63d6a54..a6a26b0cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -189,7 +189,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/src/Configuration.php b/src/Configuration.php index c77e9a701..62bdcc623 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -119,7 +119,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; } diff --git a/src/Test/Factories.php b/src/Test/Factories.php index d04aae29f..2d325b181 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -32,15 +32,15 @@ trait Factories #[Before(5)] public function _beforeHook(): void { - $this->_loadDataProvidedProxies(); // todo remove - - if (FoundryExtension::isEnabled()) { - trigger_deprecation('zenstruck/foundry', '2.7', sprintf('Trait %s is deprecated and will be removed in Foundry 3.', Factories::class)); + if (!FoundryExtension::isEnabled()) { + $this->_bootFoundry(); return; } - $this->_bootFoundry(); + trigger_deprecation('zenstruck/foundry', '2.7', sprintf('Trait %s is deprecated and will be removed in Foundry 3.', Factories::class)); + + $this->_loadDataProvidedProxies(); // todo remove } /** diff --git a/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php b/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php index c77c72559..1a7f3fec6 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php +++ b/tests/Integration/ForceFactoriesTraitUsage/AnotherBaseTestCase.php @@ -18,6 +18,8 @@ abstract class AnotherBaseTestCase extends KernelTestCase { + use SkipWithPHPUnitExtension; + // @phpstan-ignore missingType.generics public function useProxyClass(Proxy $proxy): void { diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php index 23e72634a..a2461a5ff 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsInWrongOrderTest.php @@ -23,7 +23,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithBothTraitsInWrongOrderTest extends KernelTestCase { - use Factories, ResetDatabase; + use Factories, ResetDatabase, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php index fd6de3f93..4d84f3e2d 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithBothTraitsTest.php @@ -23,7 +23,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithBothTraitsTest extends KernelTestCase { - use Factories, ResetDatabase; + use Factories, ResetDatabase, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php index 80d40751d..fdb7cdf4e 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithFactoriesTraitTest.php @@ -22,7 +22,7 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestCaseWithFactoriesTraitTest extends KernelTestCase { - use Factories; + use Factories, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php index 8d36fa425..6681111fd 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest.php @@ -22,4 +22,5 @@ final class KernelTestCaseWithOnlyResetDatabaseTraitTestWithoutFactoriesTest ext { use KernelTestCaseWithoutFactoriesTrait; use ResetDatabase; + use SkipWithPHPUnitExtension; } diff --git a/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php b/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php index 87e56c8fe..ca9bf444d 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/KernelTestWithoutFactoriesTest.php @@ -19,5 +19,5 @@ #[RequiresPhpunit('>=11.0')] final class KernelTestWithoutFactoriesTest extends KernelTestCase { - use KernelTestCaseWithoutFactoriesTrait; + use KernelTestCaseWithoutFactoriesTrait, SkipWithPHPUnitExtension; } diff --git a/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php b/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php new file mode 100644 index 000000000..64ca8f8b9 --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php @@ -0,0 +1,21 @@ +=11.0')] final class UnitTestCaseWithFactoriesTraitTest extends TestCase { - use Factories; + use Factories, SkipWithPHPUnitExtension; #[Test] public function should_not_throw(): void diff --git a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php index 2275b1af3..9a8e1c043 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php +++ b/tests/Integration/ForceFactoriesTraitUsage/UnitTestCaseWithoutFactoriesTraitTest.php @@ -22,6 +22,8 @@ #[RequiresPhpunit('>=11.0')] final class UnitTestCaseWithoutFactoriesTraitTest extends TestCase { + use SkipWithPHPUnitExtension; + #[Test] public function should_throw(): void { diff --git a/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.php new file mode 100644 index 000000000..9b9c43cfd --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/KernelTestCaseWithoutFactoriesTraitTest.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\Tests\Integration\ForceFactoriesTraitUsage\UsingExtension; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class KernelTestCaseWithoutFactoriesTraitTest extends KernelTestCase +{ + #[Test] + public function should_not_throw(): void + { + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } + + #[Test] + public function should_not_throw_even_when_kernel_is_booted(): void + { + self::getContainer()->get('.zenstruck_foundry.configuration'); + + Object1Factory::createOne(); + + $this->expectNotToPerformAssertions(); + } +} diff --git a/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php new file mode 100644 index 000000000..26fa7d1ca --- /dev/null +++ b/tests/Integration/ForceFactoriesTraitUsage/UsingExtension/UnitTestCaseWithoutFactoriesTraitTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage\UsingExtension; + +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\TestCase; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; + +#[RequiresPhpunit('>=11.0')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class UnitTestCaseWithoutFactoriesTraitTest extends TestCase +{ + #[Test] + public function should_not_throw(): void + { + $this->expectNotToPerformAssertions(); + + Object1Factory::createOne(); + } +} From 208547adecba7b821dde9d1b355fc57a4cbfa9b7 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 22 Sep 2025 11:56:46 +0200 Subject: [PATCH 3/6] feat: trigger persistence from data providers without Factories trait --- phpstan.neon | 4 - phpunit-deprecation-baseline.xml | 4 +- src/Configuration.php | 2 +- .../BootFoundryOnPreparationStarted.php | 7 +- src/PHPUnit/BuildStoryOnTestPrepared.php | 4 +- .../BootFoundryOnDataProviderMethodCalled.php | 10 +- ...ownFoundryOnDataProviderMethodFinished.php | 3 +- ...rDataProviderPersistenceOnTestPrepared.php | 44 ++++++++ src/PHPUnit/FoundryExtension.php | 4 + src/Persistence/PersistentObjectFactory.php | 2 +- ...rsistentObjectFromDataProviderRegistry.php | 102 ++++++++++++++++++ .../PersistentProxyObjectFactory.php | 2 +- src/Persistence/ProxyGenerator.php | 38 +++---- src/Test/Factories.php | 13 +-- ...rWithPersistentFactoryInKernelTestCase.php | 14 ++- ...est.php => GenericDocumentFactoryTest.php} | 2 +- ...yTest.php => GenericEntityFactoryTest.php} | 2 +- 17 files changed, 208 insertions(+), 49 deletions(-) rename src/PHPUnit/{ => DataProvider}/BootFoundryOnDataProviderMethodCalled.php (80%) rename src/PHPUnit/{ => DataProvider}/ShutdownFoundryOnDataProviderMethodFinished.php (87%) create mode 100644 src/PHPUnit/DataProvider/TriggerDataProviderPersistenceOnTestPrepared.php create mode 100644 src/Persistence/PersistentObjectFromDataProviderRegistry.php rename tests/Integration/DataProvider/{GenericDocumentProxyFactoryTest.php => GenericDocumentFactoryTest.php} (92%) rename tests/Integration/DataProvider/{GenericEntityProxyFactoryTest.php => GenericEntityFactoryTest.php} (92%) diff --git a/phpstan.neon b/phpstan.neon index f08062689..7875aa849 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 fe499d5e1..ab0464370 100644 --- a/phpunit-deprecation-baseline.xml +++ b/phpunit-deprecation-baseline.xml @@ -12,8 +12,8 @@ - - + + diff --git a/src/Configuration.php b/src/Configuration.php index 62bdcc623..7b5c52806 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -138,7 +138,7 @@ public static function boot(\Closure|self $configuration): void self::$instance = $configuration; if (FoundryExtension::shouldBeEnabled()) { - trigger_deprecation('zenstruck/foundry', '2.7', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3.'); + trigger_deprecation('zenstruck/foundry', '2.8', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3.'); } } diff --git a/src/PHPUnit/BootFoundryOnPreparationStarted.php b/src/PHPUnit/BootFoundryOnPreparationStarted.php index a7a773f0e..56f03527a 100644 --- a/src/PHPUnit/BootFoundryOnPreparationStarted.php +++ b/src/PHPUnit/BootFoundryOnPreparationStarted.php @@ -27,11 +27,14 @@ final class BootFoundryOnPreparationStarted implements Event\Test\PreparationSta { public function notify(Event\Test\PreparationStarted $event): void { - if (!$event->test()->isTestMethod()) { + $test = $event->test(); + + if (!$test->isTestMethod()) { return; } + /** @var Event\Code\TestMethod $test */ - $this->bootFoundry($event->test()->className()); + $this->bootFoundry($test->className()); } /** diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php index ff3ea9eb4..4e42376da 100644 --- a/src/PHPUnit/BuildStoryOnTestPrepared.php +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -47,7 +47,9 @@ 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()); + if (!FoundryExtension::isEnabled()) { + FactoriesTraitNotUsed::throwIfClassDoesNotHaveFactoriesTrait($test->className()); + } foreach ($withStoryAttributes as $withStoryAttribute) { $withStoryAttribute->newInstance()->story::load(); diff --git a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php similarity index 80% rename from src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php rename to src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php index e224c55d1..f967a92c5 100644 --- a/src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php +++ b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php @@ -11,13 +11,15 @@ * file that was distributed with this source code. */ -namespace Zenstruck\Foundry\PHPUnit; +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; /** @@ -30,6 +32,12 @@ 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())) { diff --git a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php similarity index 87% rename from src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php rename to src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php index ed5f29f9e..690ecfa7f 100644 --- a/src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php +++ b/src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php @@ -11,10 +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 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 fbf762e0c..ea18c5252 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -17,6 +17,9 @@ 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 @@ -50,6 +53,7 @@ public function bootstrap( // 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); 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..79ab1e713 --- /dev/null +++ b/src/Persistence/PersistentObjectFromDataProviderRegistry.php @@ -0,0 +1,102 @@ +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 */ + private array $objectsBuffer = []; + + private bool $shouldReturnExistingObject = false; + + public static function instance(): self + { + return self::$instance ?? self::$instance = new self(); + } + + /** + * @param callable():iterable $dataProviderResult + */ + public function addDataset(string $className, string $methodName, callable $dataProviderResult): void + { + $this->shouldReturnExistingObject = false; + + $dataProviderResult = $dataProviderResult(); + + if (!is_array($dataProviderResult)) { + $dataProviderResult = iterator_to_array($dataProviderResult); + } + + $testCaseContext = $this->testCaseContext($className, $methodName); + $this->datasets[$testCaseContext] = $dataProviderResult; + + $this->shouldReturnExistingObject = true; + } + + /** + * @template T of object + * + * @param PersistentObjectFactory $factory + * + * @return ($factory is PersistentProxyObjectFactory ? T&Proxy : T) + */ + public function deferObjectCreation(PersistentObjectFactory $factory): object + { + if (!$this->shouldReturnExistingObject) { + return $this->objectsBuffer[] = ProxyGenerator::wrapFactory($factory); + } + + return array_shift($this->objectsBuffer); // @phpstan-ignore return.type + } + + public function triggerPersistenceForDataset(string $className, string $methodName, int|string $dataSetName): void + { + $testCaseContext = $this->testCaseContext($className, $methodName); + + if (!isset($this->datasets[$testCaseContext][$dataSetName])) { + throw new \LogicException("No data found for test case context \"{$testCaseContext}\" with dataset name \"{$dataSetName}\"."); + } + + initialize_proxy_object($this->datasets[$testCaseContext][$dataSetName]); + + unset($this->datasets[$testCaseContext][$dataSetName]); + } + + private function testCaseContext(string $className, string $methodName): string + { + return "{$className}::{$methodName}"; + } +} diff --git a/src/Persistence/PersistentProxyObjectFactory.php b/src/Persistence/PersistentProxyObjectFactory.php index 583607cde..a7ed99283 100644 --- a/src/Persistence/PersistentProxyObjectFactory.php +++ b/src/Persistence/PersistentProxyObjectFactory.php @@ -42,7 +42,7 @@ final public function create(callable|array $attributes = []): object { $configuration = Configuration::instance(); if ($configuration->inADataProvider() && $this->isPersisting()) { - return ProxyGenerator::wrapFactory($this, $attributes); + return PersistentObjectFromDataProviderRegistry::instance()->deferObjectCreation($this->with($attributes)); } return proxy(parent::create($attributes)); // @phpstan-ignore function.unresolvableReturnType diff --git a/src/Persistence/ProxyGenerator.php b/src/Persistence/ProxyGenerator.php index 27aeab986..21df22311 100644 --- a/src/Persistence/ProxyGenerator.php +++ b/src/Persistence/ProxyGenerator.php @@ -47,47 +47,37 @@ public static function wrap(object $object): Proxy return self::generateClassFor($object)::createLazyProxy(static fn() => $object); // @phpstan-ignore staticMethod.unresolvableReturnType } - /** - * @template T of object - * - * @param PersistentProxyObjectFactory $factory - * @phpstan-param Attributes $attributes - * - * @return T&Proxy - */ - public static function wrapFactory(PersistentProxyObjectFactory $factory, callable|array $attributes): Proxy - { - return self::generateClassFor($factory)::createLazyProxy(static function() use ($factory, $attributes) { // @phpstan-ignore staticMethod.notFound - if (Configuration::instance()->inADataProvider() && $factory->isPersisting()) { - throw new \LogicException('Cannot access to a persisted object from a data provider.'); - } - - return self::unwrap($factory->create($attributes)); - }); - } - /** * @template T of object * * @param PersistentObjectFactory $factory - * @phpstan-param Attributes $attributes * - * @return T + * @return ($factory is PersistentProxyObjectFactory ? T&Proxy : T) */ - public static function wrapFactoryNativeProxy(PersistentObjectFactory $factory, callable|array $attributes): object + public static function wrapFactory(PersistentObjectFactory $factory): object { + if ($factory instanceof PersistentProxyObjectFactory) { + return self::generateClassFor($factory)::createLazyProxy(static function() use ($factory) { // @phpstan-ignore staticMethod.notFound + if (Configuration::instance()->inADataProvider() && $factory->isPersisting()) { + throw new \LogicException('Cannot access to a persisted object from a data provider.'); + } + + return ProxyGenerator::unwrap($factory->create()); + }); + } + if (\PHP_VERSION_ID < 80400) { throw new \LogicException('Native proxy generation requires PHP 8.4 or higher.'); } $reflector = new \ReflectionClass($factory::class()); - return $reflector->newLazyProxy(static function() use ($factory, $attributes) { + return $reflector->newLazyProxy(static function() use ($factory) { if (Configuration::instance()->inADataProvider() && $factory->isPersisting()) { throw new \LogicException('Cannot access to a persisted object from a data provider.'); } - return $factory->create($attributes); + return $factory->create(); }); } diff --git a/src/Test/Factories.php b/src/Test/Factories.php index 2d325b181..5919bce00 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -32,15 +32,14 @@ trait Factories #[Before(5)] public function _beforeHook(): void { - if (!FoundryExtension::isEnabled()) { - $this->_bootFoundry(); + if (FoundryExtension::isEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.8', sprintf('Trait %s is deprecated and will be removed in Foundry 3.', Factories::class)); return; } - trigger_deprecation('zenstruck/foundry', '2.7', sprintf('Trait %s is deprecated and will be removed in Foundry 3.', Factories::class)); - - $this->_loadDataProvidedProxies(); // todo remove + $this->_bootFoundry(); + $this->_loadDataProvidedProxies(); } /** @@ -107,7 +106,9 @@ private function _loadDataProvidedProxies(): void return; } - $providedData = \method_exists($this, 'getProvidedData') ? $this->getProvidedData() : $this->providedData(); // @phpstan-ignore method.notFound, method.internal + $providedData = \method_exists($this, 'getProvidedData') // @phpstan-ignore function.impossibleType + ? $this->getProvidedData() // @phpstan-ignore method.notFound + : $this->providedData(); initialize_proxy_object($providedData); } diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php index 1a6f07194..0f721c817 100644 --- a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php +++ b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php @@ -29,6 +29,7 @@ use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; +use function Zenstruck\Foundry\Persistence\assert_persisted; /** * @author Nicolas PHILIPPE @@ -51,6 +52,9 @@ public function assert_it_can_create_one_object_in_data_provider(?GenericModel $ self::assertInstanceOf(Proxy::class, $providedData); self::assertNotInstanceOf(Proxy::class, ProxyGenerator::unwrap($providedData)); // asserts two proxies are not nested self::assertSame('value set in data provider', $providedData->getProp1()); + + static::proxyFactory()::assert()->count(1); + $providedData->_assertPersisted(); } public static function createOneProxyObjectInDataProvider(): iterable @@ -136,20 +140,24 @@ public static function throwsExceptionWhenCreatingObjectInDataProvider(): iterab #[RequiresPhp('>=8.4')] public function assert_it_can_create_one_object_in_data_provider_without_proxy_with_php_84(mixed $providedData): void { - static::proxyFactory()::assert()->count(1); + static::factory()::assert()->count(1); + self::assertNotInstanceOf(Proxy::class, $providedData); self::assertInstanceOf(GenericModel::class, $providedData); self::assertSame('value set in data provider', $providedData->getProp1()); + + assert_persisted($providedData); + static::factory()::assert()->count(1); } public static function createOneObjectInDataProvider(): iterable { yield 'createOne()' => [ - static::proxyFactory()::createOne(['prop1' => 'value set in data provider']), + static::factory()::createOne(['prop1' => 'value set in data provider']), ]; yield 'create()' => [ - static::proxyFactory()->create(['prop1' => 'value set in data provider']), + static::factory()->create(['prop1' => 'value set in data provider']), ]; } diff --git a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php b/tests/Integration/DataProvider/GenericDocumentFactoryTest.php similarity index 92% rename from tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php rename to tests/Integration/DataProvider/GenericDocumentFactoryTest.php index f18c5136b..34c0f607a 100644 --- a/tests/Integration/DataProvider/GenericDocumentProxyFactoryTest.php +++ b/tests/Integration/DataProvider/GenericDocumentFactoryTest.php @@ -27,7 +27,7 @@ #[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[IgnoreDeprecations] -final class GenericDocumentProxyFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase +final class GenericDocumentFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase { use RequiresMongo; diff --git a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php b/tests/Integration/DataProvider/GenericEntityFactoryTest.php similarity index 92% rename from tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php rename to tests/Integration/DataProvider/GenericEntityFactoryTest.php index 1f5eadb08..aa3412575 100644 --- a/tests/Integration/DataProvider/GenericEntityProxyFactoryTest.php +++ b/tests/Integration/DataProvider/GenericEntityFactoryTest.php @@ -27,7 +27,7 @@ #[RequiresPhpunit('>=11.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] #[IgnoreDeprecations] -final class GenericEntityProxyFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase +final class GenericEntityFactoryTest extends DataProviderWithPersistentFactoryInKernelTestCase { use RequiresORM; From ca511101bd8a570f11dfa83d2b493acb0a1929a5 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Mon, 22 Sep 2025 21:42:08 +0200 Subject: [PATCH 4/6] chore: update docs + rector set --- UPGRADE-2.7.md | 2 +- UPGRADE-2.8.md | 53 +++++++++++++++++++ docs/index.rst | 50 +++++++++++------ phpunit-deprecation-baseline.xml | 5 +- src/Configuration.php | 2 +- src/PHPUnit/BuildStoryOnTestPrepared.php | 5 -- src/PHPUnit/FoundryExtension.php | 4 +- src/Test/Factories.php | 5 +- .../{foundry-set.php => foundry-2.7.php} | 0 utils/rector/config/foundry-2.8.php | 25 +++++++++ utils/rector/src/FoundrySetList.php | 11 +++- utils/rector/tests/AllRules/AllRulesTest.php | 2 +- 12 files changed, 131 insertions(+), 33 deletions(-) create mode 100644 UPGRADE-2.8.md rename utils/rector/config/{foundry-set.php => foundry-2.7.php} (100%) create mode 100644 utils/rector/config/foundry-2.8.php 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/docs/index.rst b/docs/index.rst index 1355a89c6..28e4c49a8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1606,28 +1606,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.8 - public function test_1(): void + 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; + + class MyTest extends WebTestCase { - $post = PostFactory::createOne(); + use Factories; - // ... + public function test_something(): void + { + $post = PostFactory::createOne(); + + // ... + } } - } Database Reset ~~~~~~~~~~~~~~ @@ -1780,7 +1798,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 { @@ -2378,8 +2396,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/phpunit-deprecation-baseline.xml b/phpunit-deprecation-baseline.xml index ab0464370..cbda2405f 100644 --- a/phpunit-deprecation-baseline.xml +++ b/phpunit-deprecation-baseline.xml @@ -12,8 +12,9 @@ - - + + + diff --git a/src/Configuration.php b/src/Configuration.php index 7b5c52806..9ff91c916 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -138,7 +138,7 @@ public static function boot(\Closure|self $configuration): void 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.'); + 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.'); } } diff --git a/src/PHPUnit/BuildStoryOnTestPrepared.php b/src/PHPUnit/BuildStoryOnTestPrepared.php index 4e42376da..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,10 +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)); } - if (!FoundryExtension::isEnabled()) { - FactoriesTraitNotUsed::throwIfClassDoesNotHaveFactoriesTrait($test->className()); - } - foreach ($withStoryAttributes as $withStoryAttribute) { $withStoryAttribute->newInstance()->story::load(); } diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index ea18c5252..852854321 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -74,12 +74,12 @@ public static function isEnabled(): bool } else { final class FoundryExtension { - public static function shouldBeEnabled(): bool + public static function shouldBeEnabled(): false { return false; } - public static function isEnabled(): bool + public static function isEnabled(): false { return false; } diff --git a/src/Test/Factories.php b/src/Test/Factories.php index 5919bce00..2eac305e0 100644 --- a/src/Test/Factories.php +++ b/src/Test/Factories.php @@ -15,7 +15,6 @@ use PHPUnit\Framework\Attributes\Before; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Configuration; - use Zenstruck\Foundry\PHPUnit\FoundryExtension; use function Zenstruck\Foundry\Persistence\initialize_proxy_object; @@ -33,7 +32,7 @@ trait Factories public function _beforeHook(): void { if (FoundryExtension::isEnabled()) { - trigger_deprecation('zenstruck/foundry', '2.8', sprintf('Trait %s is deprecated and will be removed in Foundry 3.', Factories::class)); + trigger_deprecation('zenstruck/foundry', '2.8', \sprintf('Trait %s is deprecated and will be removed in Foundry 3. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.8.md to upgrade.', Factories::class)); return; } @@ -108,7 +107,7 @@ private function _loadDataProvidedProxies(): void $providedData = \method_exists($this, 'getProvidedData') // @phpstan-ignore function.impossibleType ? $this->getProvidedData() // @phpstan-ignore method.notFound - : $this->providedData(); + : $this->providedData(); // @phpstan-ignore method.internal initialize_proxy_object($providedData); } diff --git a/utils/rector/config/foundry-set.php b/utils/rector/config/foundry-2.7.php similarity index 100% rename from utils/rector/config/foundry-set.php rename to utils/rector/config/foundry-2.7.php diff --git a/utils/rector/config/foundry-2.8.php b/utils/rector/config/foundry-2.8.php new file mode 100644 index 000000000..5f505c91d --- /dev/null +++ b/utils/rector/config/foundry-2.8.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Rector\Removing\Rector\Class_\RemoveTraitUseRector; +use Zenstruck\Foundry\Test\Factories; + +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->ruleWithConfiguration( + RemoveTraitUseRector::class, + [ + Factories::class, + ] + ); +}; diff --git a/utils/rector/src/FoundrySetList.php b/utils/rector/src/FoundrySetList.php index 86f51cf37..d406e41c2 100644 --- a/utils/rector/src/FoundrySetList.php +++ b/utils/rector/src/FoundrySetList.php @@ -13,6 +13,15 @@ final class FoundrySetList { + /** + * @deprecated use FoundrySetList::FOUNDRY_2_7 + * @var string + */ + public const REMOVE_PROXIES = self::FOUNDRY_2_7; + + /** @var string */ + public const FOUNDRY_2_7 = __DIR__.'/../config/foundry-2.7.php'; + /** @var string */ - public const REMOVE_PROXIES = __DIR__.'/../config/foundry-set.php'; + public const FOUNDRY_2_8 = __DIR__.'/../config/foundry-2.8.php'; } diff --git a/utils/rector/tests/AllRules/AllRulesTest.php b/utils/rector/tests/AllRules/AllRulesTest.php index 2fdcdb935..520891d12 100644 --- a/utils/rector/tests/AllRules/AllRulesTest.php +++ b/utils/rector/tests/AllRules/AllRulesTest.php @@ -24,6 +24,6 @@ public static function provideData(): \Iterator public function provideConfigFilePath(): string { - return __DIR__.'/../../config/foundry-set.php'; + return __DIR__.'/../../config/foundry-2.7.php'; } } From f6e588cded60eeeaa0c3beeb8900708021482af1 Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 23 Sep 2025 11:22:17 +0200 Subject: [PATCH 5/6] feat: deprecate ResetDatabase trait --- .env | 3 + .github/workflows/ci.yml | 1 + README.md | 3 +- UPGRADE-2.9.md | 53 +++++++++ docs/index.rst | 51 +++++---- phpunit-deprecation-baseline.xml | 6 + src/Attribute/ResetDatabase.php | 19 ++++ src/Configuration.php | 3 +- src/PHPUnit/AttributeReader.php | 42 +++++++ .../BootFoundryOnPreparationStarted.php | 5 +- src/PHPUnit/BuildStoryOnTestPrepared.php | 17 +-- .../BootFoundryOnDataProviderMethodCalled.php | 4 +- src/PHPUnit/FoundryExtension.php | 26 +++-- src/PHPUnit/KernelTestCaseHelper.php | 44 +++++++- .../ResetDatabaseBeforeEachTest.php | 49 ++++++++ .../ResetDatabaseBeforeFirstTest.php | 53 +++++++++ ...rsistentObjectFromDataProviderRegistry.php | 15 ++- src/Test/ResetDatabase.php | 28 +++++ .../ResetDatabase/ResetDatabaseTestKernel.php | 2 + ...rWithPersistentFactoryInKernelTestCase.php | 1 + .../SkipWithPHPUnitExtension.php | 9 ++ .../ResetDatabase/ResetDatabaseTest.php | 84 +------------- .../ResetDatabase/ResetDatabaseTestCase.php | 6 +- .../ResetDatabase/ResetDatabaseTestsTrait.php | 105 ++++++++++++++++++ .../ResetDatabaseWithTraitTest.php | 25 +++++ 25 files changed, 511 insertions(+), 143 deletions(-) create mode 100644 UPGRADE-2.9.md create mode 100644 src/Attribute/ResetDatabase.php create mode 100644 src/PHPUnit/AttributeReader.php create mode 100644 src/PHPUnit/ResetDatabase/ResetDatabaseBeforeEachTest.php create mode 100644 src/PHPUnit/ResetDatabase/ResetDatabaseBeforeFirstTest.php create mode 100644 tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php create mode 100644 tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php 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 a6a26b0cd..9de0de0c8 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' || '' }} 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.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 28e4c49a8..4c4ff6d03 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1650,33 +1650,46 @@ Add Foundry's `PHPUnit Extension`_ in your `phpunit.xml` file: 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 @@ -2048,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:: @@ -2071,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:: @@ -2267,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. :: @@ -2612,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: @@ -2634,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/phpunit-deprecation-baseline.xml b/phpunit-deprecation-baseline.xml index cbda2405f..5448ab66e 100644 --- a/phpunit-deprecation-baseline.xml +++ b/phpunit-deprecation-baseline.xml @@ -7,10 +7,16 @@ + + + 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 9ff91c916..55c3f4267 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -19,9 +19,8 @@ use Zenstruck\Foundry\InMemory\CannotEnableInMemory; use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry; use Zenstruck\Foundry\Persistence\PersistenceManager; -use Zenstruck\Foundry\PHPUnit\FoundryExtension; -use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Kevin Bond 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/BootFoundryOnPreparationStarted.php b/src/PHPUnit/BootFoundryOnPreparationStarted.php index 56f03527a..aaea4a092 100644 --- a/src/PHPUnit/BootFoundryOnPreparationStarted.php +++ b/src/PHPUnit/BootFoundryOnPreparationStarted.php @@ -33,7 +33,6 @@ public function notify(Event\Test\PreparationStarted $event): void return; } /** @var Event\Code\TestMethod $test */ - $this->bootFoundry($test->className()); } @@ -55,11 +54,11 @@ private function bootFoundry(string $className): void // integration test Configuration::boot(static function() use ($className): Configuration { - if (!KernelTestCaseHelper::getContainerForTestClass($className)->has('.zenstruck_foundry.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::getContainerForTestClass($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type + 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 8689dbfee..e68c6dc5d 100644 --- a/src/PHPUnit/BuildStoryOnTestPrepared.php +++ b/src/PHPUnit/BuildStoryOnTestPrepared.php @@ -34,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), ]; @@ -50,19 +50,4 @@ public function notify(Event\Test\Prepared $event): void $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 index f967a92c5..c94209151 100644 --- a/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php +++ b/src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php @@ -63,11 +63,11 @@ private function bootFoundryForDataProvider(string $className): void // integration test Configuration::bootForDataProvider(static function() use ($className): Configuration { - if (!KernelTestCaseHelper::getContainerForTestClass($className)->has('.zenstruck_foundry.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::getContainerForTestClass($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type + return KernelTestCaseHelper::getContainer($className)->get('.zenstruck_foundry.configuration'); // @phpstan-ignore return.type }); } } diff --git a/src/PHPUnit/FoundryExtension.php b/src/PHPUnit/FoundryExtension.php index 852854321..baf5e8e3c 100644 --- a/src/PHPUnit/FoundryExtension.php +++ b/src/PHPUnit/FoundryExtension.php @@ -20,13 +20,14 @@ 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 */ - -if (interface_exists(Runner\Extension\Extension::class)) { +if (\interface_exists(Runner\Extension\Extension::class)) { final class FoundryExtension implements Runner\Extension\Extension { private static bool $enabled = false; @@ -41,21 +42,28 @@ public function bootstrap( 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(), ]; - 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; diff --git a/src/PHPUnit/KernelTestCaseHelper.php b/src/PHPUnit/KernelTestCaseHelper.php index 12f7ff4ca..0b1e9615a 100644 --- a/src/PHPUnit/KernelTestCaseHelper.php +++ b/src/PHPUnit/KernelTestCaseHelper.php @@ -1,10 +1,20 @@ + * + * 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 @@ -14,7 +24,7 @@ final class KernelTestCaseHelper /** * @param class-string $class */ - public static function getContainerForTestClass(string $class): Container + 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)); @@ -42,4 +52,36 @@ public static function tearDownClass(string $class): void 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/Persistence/PersistentObjectFromDataProviderRegistry.php b/src/Persistence/PersistentObjectFromDataProviderRegistry.php index 79ab1e713..740e21e4d 100644 --- a/src/Persistence/PersistentObjectFromDataProviderRegistry.php +++ b/src/Persistence/PersistentObjectFromDataProviderRegistry.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Persistence; /** @@ -56,8 +65,8 @@ public function addDataset(string $className, string $methodName, callable $data $dataProviderResult = $dataProviderResult(); - if (!is_array($dataProviderResult)) { - $dataProviderResult = iterator_to_array($dataProviderResult); + if (!\is_array($dataProviderResult)) { + $dataProviderResult = \iterator_to_array($dataProviderResult); } $testCaseContext = $this->testCaseContext($className, $methodName); @@ -79,7 +88,7 @@ public function deferObjectCreation(PersistentObjectFactory $factory): object return $this->objectsBuffer[] = ProxyGenerator::wrapFactory($factory); } - return array_shift($this->objectsBuffer); // @phpstan-ignore return.type + return \array_shift($this->objectsBuffer); // @phpstan-ignore return.type } public function triggerPersistenceForDataset(string $className, string $methodName, int|string $dataSetName): void diff --git a/src/Test/ResetDatabase.php b/src/Test/ResetDatabase.php index f07cf1e1b..d9dfb8fb4 100644 --- a/src/Test/ResetDatabase.php +++ b/src/Test/ResetDatabase.php @@ -14,7 +14,10 @@ use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\BeforeClass; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Attribute\ResetDatabase as ResetDatabaseAttribute; use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager; +use Zenstruck\Foundry\PHPUnit\AttributeReader; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; /** * @author Kevin Bond @@ -28,6 +31,14 @@ trait ResetDatabase #[BeforeClass] public static function _resetDatabaseBeforeFirstTest(): void { + if (FoundryExtension::isEnabled()) { + trigger_deprecation('zenstruck/foundry', '2.9', \sprintf('Trait "%s" is deprecated and will be removed in Foundry 3. Use attribute "%s" instead. See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.9.md to upgrade.', ResetDatabase::class, ResetDatabaseAttribute::class)); + + if (self::_classHasResetDatabaseAttribute()) { + return; + } + } + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType throw new \RuntimeException(\sprintf('The "%s" trait can only be used on TestCases that extend "%s".', __TRAIT__, KernelTestCase::class)); } @@ -45,6 +56,10 @@ public static function _resetDatabaseBeforeFirstTest(): void #[Before(10)] public static function _resetDatabaseBeforeEachTest(): void { + if (FoundryExtension::isEnabled() && self::_classHasResetDatabaseAttribute()) { + return; + } + if (!\is_subclass_of(static::class, KernelTestCase::class)) { // @phpstan-ignore function.alreadyNarrowedType throw new \RuntimeException(\sprintf('The "%s" trait can only be used on TestCases that extend "%s".', __TRAIT__, KernelTestCase::class)); } @@ -54,4 +69,17 @@ public static function _resetDatabaseBeforeEachTest(): void static fn() => static::ensureKernelShutdown(), ); } + + /** + * @internal + */ + private static function _classHasResetDatabaseAttribute(): bool + { + $resetDatabaseAttributes = AttributeReader::collectAttributesFromClassAndParents( + ResetDatabaseAttribute::class, + new \ReflectionClass(static::class) + ); + + return [] !== $resetDatabaseAttributes; + } } diff --git a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php index 2cd3d777b..f00eb4453 100644 --- a/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php +++ b/tests/Fixture/ResetDatabase/ResetDatabaseTestKernel.php @@ -38,6 +38,8 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load parent::configureContainer($c, $loader); $c->loadFromExtension('zenstruck_foundry', [ + 'persistence' => ['flush_once' => true], + 'enable_auto_refresh_with_lazy_objects' => self::usePHP84LazyObjects(), 'global_state' => [ GlobalStory::class, GlobalInvokableService::class, diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php index 0f721c817..d37e9bfb5 100644 --- a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php +++ b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryInKernelTestCase.php @@ -29,6 +29,7 @@ use Zenstruck\Foundry\Test\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory; use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + use function Zenstruck\Foundry\Persistence\assert_persisted; /** diff --git a/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php b/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php index 64ca8f8b9..f489d99cf 100644 --- a/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php +++ b/tests/Integration/ForceFactoriesTraitUsage/SkipWithPHPUnitExtension.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage; use PHPUnit\Framework\Attributes\Before; diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTest.php b/tests/Integration/ResetDatabase/ResetDatabaseTest.php index 0377ef93f..7ba1643e2 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTest.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTest.php @@ -15,99 +15,17 @@ use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\Attributes\Test; -use Symfony\Bundle\FrameworkBundle\Console\Application; -use Symfony\Component\Console\Input\ArrayInput; -use Symfony\Component\Console\Output\BufferedOutput; use Zenstruck\Foundry\Persistence\PersistenceManager; -use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; -use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; -use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\MongoResetterDecorator; use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\OrmResetterDecorator; -use function Zenstruck\Foundry\Persistence\persist; -use function Zenstruck\Foundry\Persistence\repository; - /** * @author Nicolas PHILIPPE */ final class ResetDatabaseTest extends ResetDatabaseTestCase { - /** - * @test - */ - #[Test] - public function it_generates_valid_schema(): void - { - $application = new Application(self::bootKernel()); - $application->setAutoExit(false); - - $exit = $application->run( - new ArrayInput(['command' => 'doctrine:schema:validate', '-v' => true]), - $output = new BufferedOutput() - ); - - if (FoundryTestKernel::usesMigrations()) { - // The command actually fails, because of a bug in doctrine ORM 3! - // https://github.com/doctrine/migrations/issues/1406 - self::assertSame(2, $exit, \sprintf('Schema is not valid: %s', $commandOutput = $output->fetch())); - self::assertStringContainsString('1 schema diff(s) detected', $commandOutput); - self::assertStringContainsString('DROP TABLE doctrine_migration_versions', $commandOutput); - } else { - self::assertSame(0, $exit, \sprintf('Schema is not valid: %s', $output->fetch())); - } - } - - /** - * @test - */ - #[Test] - public function it_can_store_object(): void - { - if (FoundryTestKernel::hasORM()) { - GenericEntityFactory::assert()->count(0); - GenericEntityFactory::createOne(); - GenericEntityFactory::assert()->count(1); - } - - if (FoundryTestKernel::hasMongo()) { - GenericDocumentFactory::assert()->count(0); - GenericDocumentFactory::createOne(); - GenericDocumentFactory::assert()->count(1); - } - } - - /** - * @test - * @depends it_can_store_object - */ - #[Test] - #[Depends('it_can_store_object')] - public function it_still_starts_from_fresh_db(): void - { - if (FoundryTestKernel::hasORM()) { - GenericEntityFactory::assert()->count(0); - } - - if (FoundryTestKernel::hasMongo()) { - GenericDocumentFactory::assert()->count(0); - } - } - - /** - * @test - */ - #[Test] - public function can_create_object_in_another_schema(): void - { - if (!\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { - self::markTestSkipped('PostgreSQL needed.'); - } - - persist(Article::class, ['title' => 'Hello World!']); - repository(Article::class)->assert()->count(1); - } + use ResetDatabaseTestsTrait; /** * @test diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php index f09625cab..7f22b0867 100644 --- a/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestCase.php @@ -14,14 +14,12 @@ namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; -use Zenstruck\Foundry\Test\Factories; -use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Attribute\ResetDatabase; use Zenstruck\Foundry\Tests\Fixture\ResetDatabase\ResetDatabaseTestKernel; +#[ResetDatabase] abstract class ResetDatabaseTestCase extends KernelTestCase { - use Factories, ResetDatabase; - protected static function getKernelClass(): string { return ResetDatabaseTestKernel::class; diff --git a/tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php b/tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php new file mode 100644 index 000000000..eb25dde44 --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseTestsTrait.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use PHPUnit\Framework\Attributes\Depends; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Zenstruck\Foundry\Tests\Fixture\EntityInAnotherSchema\Article; +use Zenstruck\Foundry\Tests\Fixture\Factories\Document\GenericDocumentFactory; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\FoundryTestKernel; + +use function Zenstruck\Foundry\Persistence\persist; +use function Zenstruck\Foundry\Persistence\repository; + +trait ResetDatabaseTestsTrait +{ + /** + * @test + */ + #[Test] + public function it_generates_valid_schema(): void + { + $application = new Application(self::bootKernel()); + $application->setAutoExit(false); + + $exit = $application->run( + new ArrayInput(['command' => 'doctrine:schema:validate', '-v' => true]), + $output = new BufferedOutput() + ); + + if (FoundryTestKernel::usesMigrations()) { + // The command actually fails, because of a bug in doctrine ORM 3! + // https://github.com/doctrine/migrations/issues/1406 + self::assertSame(2, $exit, \sprintf('Schema is not valid: %s', $commandOutput = $output->fetch())); + self::assertStringContainsString('1 schema diff(s) detected', $commandOutput); + self::assertStringContainsString('DROP TABLE doctrine_migration_versions', $commandOutput); + } else { + self::assertSame(0, $exit, \sprintf('Schema is not valid: %s', $output->fetch())); + } + } + + /** + * @test + */ + #[Test] + public function it_can_store_object(): void + { + if (FoundryTestKernel::hasORM()) { + GenericEntityFactory::assert()->count(0); + GenericEntityFactory::createOne(); + GenericEntityFactory::assert()->count(1); + } + + if (FoundryTestKernel::hasMongo()) { + GenericDocumentFactory::assert()->count(0); + GenericDocumentFactory::createOne(); + GenericDocumentFactory::assert()->count(1); + } + } + + /** + * @test + * @depends it_can_store_object + */ + #[Test] + #[Depends('it_can_store_object')] + public function it_still_starts_from_fresh_db(): void + { + if (FoundryTestKernel::hasORM()) { + GenericEntityFactory::assert()->count(0); + } + + if (FoundryTestKernel::hasMongo()) { + GenericDocumentFactory::assert()->count(0); + } + } + + /** + * @test + */ + #[Test] + public function can_create_object_in_another_schema(): void + { + if (!\str_starts_with(\getenv('DATABASE_URL') ?: '', 'postgresql')) { + self::markTestSkipped('PostgreSQL needed.'); + } + + persist(Article::class, ['title' => 'Hello World!']); + repository(Article::class)->assert()->count(1); + } +} diff --git a/tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php b/tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php new file mode 100644 index 000000000..75d074caa --- /dev/null +++ b/tests/Integration/ResetDatabase/ResetDatabaseWithTraitTest.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\ResetDatabase; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Test\ResetDatabase; + +/** + * @author Nicolas PHILIPPE + */ +final class ResetDatabaseWithTraitTest extends KernelTestCase +{ + use ResetDatabase, ResetDatabaseTestsTrait; +} From 305446800ad217d093720ff20dd4c5222eb98a6b Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Tue, 23 Sep 2025 12:06:16 +0200 Subject: [PATCH 6/6] feat: add a rector set for v2.9 --- utils/rector/config/foundry-2.9.php | 25 ++++++++ .../src/ResetDatabaseAttributeRector.php | 60 +++++++++++++++++++ .../add_atttribute_to_attribute_group.php.inc | 25 ++++++++ .../change_trait_to_attribute.php.inc | 23 +++++++ .../Fixtures/do_nothing_if_no_trait.php.inc | 9 +++ .../Fixtures/dont_add_attribute_twice.php.inc | 24 ++++++++ .../removes_only_reset_dataase_trait.php.inc | 47 +++++++++++++++ .../ResetDatabaseAttributeRectorTest.php | 38 ++++++++++++ .../tests/ResetDatabaseAttribute/config.php | 21 +++++++ 9 files changed, 272 insertions(+) create mode 100644 utils/rector/config/foundry-2.9.php create mode 100644 utils/rector/src/ResetDatabaseAttributeRector.php create mode 100644 utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc create mode 100644 utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc create mode 100644 utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc create mode 100644 utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc create mode 100644 utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc create mode 100644 utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php create mode 100644 utils/rector/tests/ResetDatabaseAttribute/config.php diff --git a/utils/rector/config/foundry-2.9.php b/utils/rector/config/foundry-2.9.php new file mode 100644 index 000000000..5f505c91d --- /dev/null +++ b/utils/rector/config/foundry-2.9.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Rector\Removing\Rector\Class_\RemoveTraitUseRector; +use Zenstruck\Foundry\Test\Factories; + +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->ruleWithConfiguration( + RemoveTraitUseRector::class, + [ + Factories::class, + ] + ); +}; diff --git a/utils/rector/src/ResetDatabaseAttributeRector.php b/utils/rector/src/ResetDatabaseAttributeRector.php new file mode 100644 index 000000000..d1010432e --- /dev/null +++ b/utils/rector/src/ResetDatabaseAttributeRector.php @@ -0,0 +1,60 @@ +> */ + public function getNodeTypes(): array + { + return [Node\Stmt\Class_::class]; + } + + /** @param Node\Stmt\Class_ $node */ + public function refactor(Node $node): Node|null + { + /** @var ?Node\Stmt\TraitUse $traitUseWithResetDatabase */ + $traitUseWithResetDatabase = $this->nodeFinder->findFirst($node->stmts, function (Node $node): bool { + return $node instanceof Node\Stmt\TraitUse + && array_any($node->traits, fn(Node\Name $name) => $this->getName($name) === ResetDatabaseTrait::class); + }); + + if (!$traitUseWithResetDatabase) { + return null; + } + + $traitUseWithResetDatabase->traits = array_filter( + $traitUseWithResetDatabase->traits, + fn(Node\Name $name) => $this->getName($name) !== ResetDatabaseTrait::class + ); + + if ($traitUseWithResetDatabase->traits === []) { + $node->stmts = array_filter($node->stmts, fn(Node\Stmt $stmt) => $stmt !== $traitUseWithResetDatabase); + } + + $hasResetDatabaseTrait = (bool)$this->nodeFinder->findFirst($node->attrGroups, function (Node $node): bool { + return $this->getName($node) === ResetDatabaseAttribute::class; + }); + + if ($hasResetDatabaseTrait) { + return $node; + } + + $node->attrGroups[] = new Node\AttributeGroup([ + new Node\Attribute(new Node\Name\FullyQualified(ResetDatabaseAttribute::class)), + ]); + + return $node; + } +} diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc new file mode 100644 index 000000000..d1bc2afbd --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/add_atttribute_to_attribute_group.php.inc @@ -0,0 +1,25 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc new file mode 100644 index 000000000..8cf92c884 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/change_trait_to_attribute.php.inc @@ -0,0 +1,23 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc new file mode 100644 index 000000000..73717ea3d --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/do_nothing_if_no_trait.php.inc @@ -0,0 +1,9 @@ + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc new file mode 100644 index 000000000..fe3d039d8 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/dont_add_attribute_twice.php.inc @@ -0,0 +1,24 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc new file mode 100644 index 000000000..f9b30024f --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/Fixtures/removes_only_reset_dataase_trait.php.inc @@ -0,0 +1,47 @@ + +----- + diff --git a/utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php b/utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php new file mode 100644 index 000000000..178ea4a75 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/ResetDatabaseAttributeRectorTest.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Utils\Rector\Tests\ResetDatabaseAttribute; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Test; +use Rector\Testing\PHPUnit\AbstractRectorTestCase; + +final class ResetDatabaseAttributeRectorTest extends AbstractRectorTestCase +{ + #[Test] + #[DataProvider('provideData')] + public function test(string $filePath): void + { + $this->doTestFile($filePath); + } + + public static function provideData(): \Iterator + { + return self::yieldFilesFromDirectory(__DIR__.'/Fixtures'); + } + + public function provideConfigFilePath(): string + { + return __DIR__.'/config.php'; + } +} diff --git a/utils/rector/tests/ResetDatabaseAttribute/config.php b/utils/rector/tests/ResetDatabaseAttribute/config.php new file mode 100644 index 000000000..0fae53c07 --- /dev/null +++ b/utils/rector/tests/ResetDatabaseAttribute/config.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Rector\Config\RectorConfig; +use Zenstruck\Foundry\Utils\Rector\ResetDatabaseAttributeRector; + +return static function (RectorConfig $rectorConfig): void { + $rectorConfig->rules( + [ResetDatabaseAttributeRector::class], + ); +};