From be382e1c8b1bf12ee78b040fc90e89b58a82ec9a Mon Sep 17 00:00:00 2001 From: Nicolas PHILIPPE Date: Wed, 21 May 2025 08:49:11 +0200 Subject: [PATCH] feat: use native proxies for object creation in data providers --- docs/index.rst | 2 +- phpstan.neon | 4 + src/Persistence/PersistentObjectFactory.php | 8 +- src/Persistence/ProxyGenerator.php | 23 +++++ src/Persistence/functions.php | 6 ++ ...ithNonProxyFactoryInKernelTestCaseTest.php | 2 + ...hPersistentFactoryAndPHP84InKernelTest.php | 98 +++++++++++++++++++ 7 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php diff --git a/docs/index.rst b/docs/index.rst index 3d655489..7ab4a278 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1163,7 +1163,7 @@ once. To do this, wrap the operations in a ``flush_after()`` callback: TagFactory::createMany(200); // instantiated/persisted but not flushed }); // single flush -The ``flush_after()`` function forwards the callback’s return, in case you need to use the objects in your tests: +The ``flush_after()`` function forwards the callback's return, in case you need to use the objects in your tests: :: diff --git a/phpstan.neon b/phpstan.neon index 53a086a0..85dfcf5f 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -69,6 +69,10 @@ parameters: - identifier: missingType.callable path: tests/Fixture/Maker/expected/ + # we're currently running static analysis with PHP 8.3 + - message: '#Call to an undefined method ReflectionClass\<(.*)\>::isUninitializedLazyObject\(\).#' + - message: '#Call to an undefined method ReflectionClass\<(.*)\>::initializeLazyObject\(\).#' + excludePaths: - tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php - tests/Fixture/Maker/expected/can_create_factory_interactively.php diff --git a/src/Persistence/PersistentObjectFactory.php b/src/Persistence/PersistentObjectFactory.php index 4d8b108b..d8b796e4 100644 --- a/src/Persistence/PersistentObjectFactory.php +++ b/src/Persistence/PersistentObjectFactory.php @@ -203,6 +203,12 @@ final public static function truncate(): void */ public function create(callable|array $attributes = []): object { + $configuration = Configuration::instance(); + + if ($configuration->inADataProvider() && \PHP_VERSION_ID >= 80400 && !$this instanceof PersistentProxyObjectFactory) { + return ProxyGenerator::wrapFactoryNativeProxy($this, $attributes); + } + $object = parent::create($attributes); foreach ($this->tempAfterInstantiate as $callback) { @@ -217,8 +223,6 @@ public function create(callable|array $attributes = []): object return $object; } - $configuration = Configuration::instance(); - if ($configuration->flushOnce && !$this->isRootFactory) { return $object; } diff --git a/src/Persistence/ProxyGenerator.php b/src/Persistence/ProxyGenerator.php index eea1a1e1..4893759f 100644 --- a/src/Persistence/ProxyGenerator.php +++ b/src/Persistence/ProxyGenerator.php @@ -59,6 +59,25 @@ public static function wrapFactory(PersistentProxyObjectFactory $factory, callab return self::generateClassFor($factory)::createLazyProxy(static fn() => unproxy($factory->create($attributes))); // @phpstan-ignore-line } + /** + * @template T of object + * + * @param PersistentObjectFactory $factory + * @phpstan-param Attributes $attributes + * + * @return T + */ + public static function wrapFactoryNativeProxy(PersistentObjectFactory $factory, callable|array $attributes): object + { + 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 fn() => $factory->create($attributes)); // @phpstan-ignore-line + } + /** * @template T * @@ -80,6 +99,10 @@ public static function unwrap(mixed $what, bool $withAutoRefresh = true): mixed return $what->_real($withAutoRefresh); // @phpstan-ignore return.type } + if (\PHP_VERSION_ID >= 80400 && is_object($what) && ($reflector = new \ReflectionClass($what))->isUninitializedLazyObject($what)) { + return $reflector->initializeLazyObject($what); + } + return $what; } diff --git a/src/Persistence/functions.php b/src/Persistence/functions.php index f8966923..4eeeb128 100644 --- a/src/Persistence/functions.php +++ b/src/Persistence/functions.php @@ -181,6 +181,12 @@ function enable_persisting(): void */ function initialize_proxy_object(mixed $what): void { + if (\PHP_VERSION_ID >= 80400 && is_object($what) && ($reflector = new \ReflectionClass($what))->isUninitializedLazyObject($what)) { + $reflector->initializeLazyObject($what); + + return; + } + match (true) { $what instanceof Proxy => $what->_initializeLazyObject(), \is_array($what) => \array_map(initialize_proxy_object(...), $what), diff --git a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php index b948ec08..3d66c024 100644 --- a/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php +++ b/tests/Integration/DataProvider/DataProviderWithNonProxyFactoryInKernelTestCaseTest.php @@ -14,6 +14,7 @@ namespace Zenstruck\Foundry\Tests\Integration\DataProvider; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhp; use PHPUnit\Framework\Attributes\RequiresPhpunit; use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; use PHPUnit\Framework\Attributes\Test; @@ -28,6 +29,7 @@ * @requires PHPUnit >=11.4 */ #[RequiresPhpunit('>=11.4')] +#[RequiresPhp('<8.4')] #[RequiresPhpunitExtension(FoundryExtension::class)] final class DataProviderWithNonProxyFactoryInKernelTestCaseTest extends KernelTestCase { diff --git a/tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php new file mode 100644 index 00000000..81ab0346 --- /dev/null +++ b/tests/Integration/DataProvider/DataProviderWithPersistentFactoryAndPHP84InKernelTest.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Zenstruck\Foundry\Tests\Integration\DataProvider; + +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; +use PHPUnit\Framework\Attributes\RequiresPhp; +use PHPUnit\Framework\Attributes\RequiresPhpunit; +use PHPUnit\Framework\Attributes\RequiresPhpunitExtension; +use PHPUnit\Framework\Attributes\Test; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory; +use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\PHPUnit\FoundryExtension; +use Zenstruck\Foundry\Test\Factories; +use Zenstruck\Foundry\Test\ResetDatabase; +use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory; +use Zenstruck\Foundry\Tests\Fixture\Model\GenericModel; + +use function Zenstruck\Foundry\Persistence\unproxy; + +/** + * @author Nicolas PHILIPPE + * @requires PHPUnit >=11.4 + */ +#[RequiresPhpunit('>=11.4')] +#[RequiresPhp('>=8.4')] +#[RequiresPhpunitExtension(FoundryExtension::class)] +final class DataProviderWithPersistentFactoryAndPHP84InKernelTest extends KernelTestCase +{ + use Factories; + use ResetDatabase; + + #[Test] + #[DataProvider('createOneObjectInDataProvider')] + public function assert_it_can_create_one_object_in_data_provider(?GenericModel $providedData): void + { + GenericEntityFactory::assert()->count(1); + + self::assertNotNull($providedData); + self::assertFalse((new \ReflectionClass($providedData))->isUninitializedLazyObject($providedData)); + self::assertSame('value set in data provider', $providedData->getProp1()); + } + + public static function createOneObjectInDataProvider(): iterable + { + yield 'createOne()' => [ + GenericEntityFactory::createOne(['prop1' => 'value set in data provider']), + ]; + + yield 'create()' => [ + GenericEntityFactory::new()->create(['prop1' => 'value set in data provider']), + ]; + } + + #[Test] + #[DataProvider('createMultipleObjectsInDataProvider')] + public function assert_it_can_create_multiple_objects_in_data_provider(?array $providedData): void + { + self::assertIsArray($providedData); + GenericEntityFactory::assert()->count(2); + + foreach ($providedData as $providedDatum) { + self::assertFalse((new \ReflectionClass($providedDatum))->isUninitializedLazyObject($providedDatum)); + } + + self::assertSame('prop 1', $providedData[0]->getProp1()); + self::assertSame('prop 2', $providedData[1]->getProp1()); + } + + public static function createMultipleObjectsInDataProvider(): iterable + { + yield 'createSequence()' => [ + GenericEntityFactory::createSequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ]), + ]; + + yield 'FactoryCollection::create()' => [ + GenericEntityFactory::new()->sequence([ + ['prop1' => 'prop 1'], + ['prop1' => 'prop 2'], + ])->create(), + ]; + } +}