Skip to content

Commit fec86ed

Browse files
committed
feat: trigger persistence from data providers without Factories trait
1 parent 40ffac4 commit fec86ed

16 files changed

+205
-48
lines changed

phpstan.neon

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ parameters:
3030
- identifier: missingType.iterableValue
3131
path: tests/
3232

33-
# We support both PHPUnit versions (this method changed in PHPUnit 10)
34-
- identifier: function.impossibleType
35-
path: src/Test/Factories.php
36-
3733
# PHPStan does not understand PHP version checks
3834
- message: '#Comparison operation "(<|>|<=|>=)" between int<80\d+, 80\d+> and 80\d+ is always (false|true).#'
3935

phpunit-deprecation-baseline.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212
<issue><![CDATA[Since zenstruck/foundry 2.7: Proxy usage is deprecated in PHP 8.4. You should extend directly PersistentObjectFactory in your factories.
1313
Foundry now leverages the native PHP lazy system to auto-refresh objects (it can be enabled with "zenstruck_foundry.enable_auto_refresh_with_lazy_objects" configuration).
1414
See https://github.com/zenstruck/foundry/blob/2.x/UPGRADE-2.7.md to upgrade.]]></issue>
15-
<issue><![CDATA[Since zenstruck/foundry 2.7: Trait Zenstruck\Foundry\Test\Factories is deprecated and will be removed in Foundry 3.]]></issue>
16-
<issue><![CDATA[Since zenstruck/foundry 2.7: Not using Foundry's PHPUnit extension is deprecated and will throw an error in Foundry 3.]]></issue>
15+
<issue><![CDATA[Since zenstruck/foundry 2.8: Trait Zenstruck\Foundry\Test\Factories is deprecated and will be removed in Foundry 3.]]></issue>
16+
<issue><![CDATA[Since zenstruck/foundry 2.8: Not using Foundry's PHPUnit extension is deprecated and will throw an error in Foundry 3.]]></issue>
1717
</line>
1818
</file>
1919
<file path="vendor/doctrine/deprecations/src/Deprecation.php">

src/Configuration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ public static function boot(\Closure|self $configuration): void
138138
self::$instance = $configuration;
139139

140140
if (FoundryExtension::shouldBeEnabled()) {
141-
trigger_deprecation('zenstruck/foundry', '2.7', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3.');
141+
trigger_deprecation('zenstruck/foundry', '2.8', 'Not using Foundry\'s PHPUnit extension is deprecated and will throw an error in Foundry 3.');
142142
}
143143
}
144144

src/PHPUnit/BootFoundryOnPreparationStarted.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,14 @@ final class BootFoundryOnPreparationStarted implements Event\Test\PreparationSta
2727
{
2828
public function notify(Event\Test\PreparationStarted $event): void
2929
{
30-
if (!$event->test()->isTestMethod()) {
30+
$test = $event->test();
31+
32+
if (!$test->isTestMethod()) {
3133
return;
3234
}
35+
/** @var Event\Code\TestMethod $test */
3336

34-
$this->bootFoundry($event->test()->className());
37+
$this->bootFoundry($test->className());
3538
}
3639

3740
/**

src/PHPUnit/BootFoundryOnDataProviderMethodCalled.php renamed to src/PHPUnit/DataProvider/BootFoundryOnDataProviderMethodCalled.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
* file that was distributed with this source code.
1212
*/
1313

14-
namespace Zenstruck\Foundry\PHPUnit;
14+
namespace Zenstruck\Foundry\PHPUnit\DataProvider;
1515

1616
use PHPUnit\Event;
1717
use PHPUnit\Framework\TestCase;
1818
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
1919
use Zenstruck\Foundry\Configuration;
2020
use Zenstruck\Foundry\InMemory\AsInMemoryTest;
21+
use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry;
22+
use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
2123
use Zenstruck\Foundry\Test\UnitTestConfig;
2224

2325
/**
@@ -30,6 +32,12 @@ public function notify(Event\Test\DataProviderMethodCalled $event): void
3032
{
3133
$this->bootFoundryForDataProvider($event->testMethod()->className());
3234

35+
PersistentObjectFromDataProviderRegistry::instance()->addDataset(
36+
$event->testMethod()->className(),
37+
$event->testMethod()->methodName(),
38+
"{$event->dataProviderMethod()->className()}::{$event->dataProviderMethod()->methodName()}"(...) // @phpstan-ignore callable.nonCallable
39+
);
40+
3341
$testMethod = $event->testMethod();
3442

3543
if (AsInMemoryTest::shouldEnableInMemory($testMethod->className(), $testMethod->methodName())) {

src/PHPUnit/ShutdownFoundryOnDataProviderMethodFinished.php renamed to src/PHPUnit/DataProvider/ShutdownFoundryOnDataProviderMethodFinished.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
* file that was distributed with this source code.
1212
*/
1313

14-
namespace Zenstruck\Foundry\PHPUnit;
14+
namespace Zenstruck\Foundry\PHPUnit\DataProvider;
1515

1616
use PHPUnit\Event;
1717
use Zenstruck\Foundry\Configuration;
18+
use Zenstruck\Foundry\PHPUnit\KernelTestCaseHelper;
1819

1920
/**
2021
* @internal
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the zenstruck/foundry package.
7+
*
8+
* (c) Kevin Bond <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace Zenstruck\Foundry\PHPUnit\DataProvider;
15+
16+
use PHPUnit\Event;
17+
use Zenstruck\Foundry\Persistence\PersistentObjectFromDataProviderRegistry;
18+
19+
/**
20+
* @internal
21+
* @author Nicolas PHILIPPE <[email protected]>
22+
*/
23+
final class TriggerDataProviderPersistenceOnTestPrepared implements Event\Test\PreparedSubscriber
24+
{
25+
public function notify(Event\Test\Prepared $event): void
26+
{
27+
$test = $event->test();
28+
29+
if (!$test->isTestMethod()) {
30+
return;
31+
}
32+
/** @var Event\Code\TestMethod $test */
33+
34+
if (!$test->testData()->hasDataFromDataProvider() || $test->metadata()->isDataProvider()->isEmpty()) {
35+
return;
36+
}
37+
38+
PersistentObjectFromDataProviderRegistry::instance()->triggerPersistenceForDataset(
39+
$test->className(),
40+
$test->methodName(),
41+
$test->testData()->dataFromDataProvider()->dataSetName(),
42+
);
43+
}
44+
}

src/PHPUnit/FoundryExtension.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
use PHPUnit\Runner;
1818
use PHPUnit\TextUI;
1919
use Zenstruck\Foundry\Configuration;
20+
use Zenstruck\Foundry\PHPUnit\DataProvider\BootFoundryOnDataProviderMethodCalled;
21+
use Zenstruck\Foundry\PHPUnit\DataProvider\ShutdownFoundryOnDataProviderMethodFinished;
22+
use Zenstruck\Foundry\PHPUnit\DataProvider\TriggerDataProviderPersistenceOnTestPrepared;
2023

2124
/**
2225
* @internal
@@ -50,6 +53,7 @@ public function bootstrap(
5053
// those deal with data provider events which can be useful only if PHPUnit >=11.4 is used
5154
$subscribers[] = new BootFoundryOnDataProviderMethodCalled();
5255
$subscribers[] = new ShutdownFoundryOnDataProviderMethodFinished();
56+
$subscribers[] = new TriggerDataProviderPersistenceOnTestPrepared();
5357
}
5458

5559
$facade->registerSubscribers(...$subscribers);

src/Persistence/PersistentObjectFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ public function create(callable|array $attributes = []): object
245245
&& $this->isPersisting()
246246
&& !$this instanceof PersistentProxyObjectFactory
247247
) {
248-
return ProxyGenerator::wrapFactoryNativeProxy($this, $attributes);
248+
return PersistentObjectFromDataProviderRegistry::instance()->deferObjectCreation($this->with($attributes));
249249
}
250250

251251
$object = parent::create($attributes);
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Persistence;
4+
5+
/**
6+
* If a persistent object has been created in a data provider, we need to initialize the proxy object,
7+
* which will trigger the object to be persisted.
8+
*
9+
* Otherwise, such test would not pass:
10+
* ```php
11+
* #[DataProvider('provide')]
12+
* public function testSomething(MyEntity $entity): void
13+
* {
14+
* MyEntityFactory::assert()->count(1);
15+
* }
16+
*
17+
* public static function provide(): iterable
18+
* {
19+
* yield [MyEntityFactory::createOne()];
20+
* }
21+
* ```
22+
*
23+
* Sadly, this cannot be done directly a subscriber, since PHPUnit does not give access to the actual tests instances.
24+
*
25+
* This class is highly hacky!
26+
* We collect all the "datasets" and we trigger the persistence for each one before the test is executed.
27+
* This means that de data providers are called twice.
28+
* To prevent the persisted object from being different from the one returned by the data provider, we use a "buffer" so
29+
* that we can return the same object for each data provider call.
30+
*
31+
* @internal
32+
*/
33+
final class PersistentObjectFromDataProviderRegistry
34+
{
35+
private static ?self $instance = null;
36+
37+
/** @var array<string, array<array-key, mixed>> */
38+
private array $datasets = [];
39+
40+
/** @var list<object> */
41+
private array $objectsBuffer = [];
42+
43+
private bool $shouldReturnExistingObject = false;
44+
45+
public static function instance(): self
46+
{
47+
return self::$instance ?? self::$instance = new self();
48+
}
49+
50+
/**
51+
* @param callable():iterable<array-key, mixed> $dataProviderResult
52+
*/
53+
public function addDataset(string $className, string $methodName, callable $dataProviderResult): void
54+
{
55+
$this->shouldReturnExistingObject = false;
56+
57+
$dataProviderResult = $dataProviderResult();
58+
59+
if (!is_array($dataProviderResult)) {
60+
$dataProviderResult = iterator_to_array($dataProviderResult);
61+
}
62+
63+
$testCaseContext = $this->testCaseContext($className, $methodName);
64+
$this->datasets[$testCaseContext] = $dataProviderResult;
65+
66+
$this->shouldReturnExistingObject = true;
67+
}
68+
69+
/**
70+
* @template T of object
71+
*
72+
* @param PersistentObjectFactory<T> $factory
73+
*
74+
* @return ($factory is PersistentProxyObjectFactory<T> ? T&Proxy<T> : T)
75+
*/
76+
public function deferObjectCreation(PersistentObjectFactory $factory): object
77+
{
78+
if (!$this->shouldReturnExistingObject) {
79+
return $this->objectsBuffer[] = ProxyGenerator::wrapFactory($factory);
80+
}
81+
82+
return array_shift($this->objectsBuffer); // @phpstan-ignore return.type
83+
}
84+
85+
public function triggerPersistenceForDataset(string $className, string $methodName, int|string $dataSetName): void
86+
{
87+
$testCaseContext = $this->testCaseContext($className, $methodName);
88+
89+
if (!isset($this->datasets[$testCaseContext][$dataSetName])) {
90+
throw new \LogicException("No data found for test case context \"{$testCaseContext}\" with dataset name \"{$dataSetName}\".");
91+
}
92+
93+
initialize_proxy_object($this->datasets[$testCaseContext][$dataSetName]);
94+
95+
unset($this->datasets[$testCaseContext][$dataSetName]);
96+
}
97+
98+
private function testCaseContext(string $className, string $methodName): string
99+
{
100+
return "{$className}::{$methodName}";
101+
}
102+
}

0 commit comments

Comments
 (0)