Skip to content

Commit 02f85f2

Browse files
committed
feat: create proxy system with PHP 8.4 lazy proxies
fix: review
1 parent 17817e9 commit 02f85f2

24 files changed

+724
-22
lines changed

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@
3535
"doctrine/doctrine-migrations-bundle": "^2.2|^3.0",
3636
"doctrine/mongodb-odm": "^2.4",
3737
"doctrine/mongodb-odm-bundle": "^4.6|^5.0",
38-
"doctrine/persistence": "^2.0|^3.0|^4.0",
3938
"doctrine/orm": "^2.16|^3.0",
39+
"doctrine/persistence": "^2.0|^3.0|^4.0",
4040
"phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0",
41+
"symfony/browser-kit": "^6.4|^7.0|^8.0",
4142
"symfony/console": "^6.4|^7.0|^8.0",
4243
"symfony/dotenv": "^6.4|^7.0|^8.0",
4344
"symfony/framework-bundle": "^6.4|^7.0|^8.0",
4445
"symfony/maker-bundle": "^1.55",
4546
"symfony/phpunit-bridge": "^6.4|^7.0|^8.0",
47+
"symfony/routing": "^6.4|^7.0|^8.0",
4648
"symfony/runtime": "^6.4|^7.0|^8.0",
4749
"symfony/translation-contracts": "^3.4",
4850
"symfony/uid": "^6.4|^7.0|^8.0",

config/persistence.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
namespace Symfony\Component\DependencyInjection\Loader\Configurator;
44

5+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
6+
use Symfony\Component\HttpKernel\Event\TerminateEvent;
7+
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
58
use Zenstruck\Foundry\Command\LoadFixturesCommand;
69
use Zenstruck\Foundry\Persistence\PersistenceManager;
10+
use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker;
711
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
812

913
return static function (ContainerConfigurator $container): void {
@@ -27,4 +31,13 @@
2731
'description' => 'Load stories which are marked with #[AsFixture] attribute.',
2832
])
2933
;
34+
35+
if (PHP_VERSION_ID >= 80400) {
36+
$container->services()->set('.foundry.persistence.objects_tracker', PersistedObjectsTracker::class)
37+
->tag('kernel.reset', ['method' => 'refresh'])
38+
->tag('kernel.event_listener', ['event' => TerminateEvent::class, 'method' => 'refresh'])
39+
->tag('kernel.event_listener', ['event' => ConsoleTerminateEvent::class, 'method' => 'refresh'])
40+
->tag('kernel.event_listener', ['event' => WorkerMessageHandledEvent::class, 'method' => 'refresh']) // @phpstan-ignore class.notFound
41+
;
42+
}
3043
};

config/services.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
param('zenstruck_foundry.persistence.flush_once'),
3636
'%env(default:zenstruck_foundry.faker.seed:int:FOUNDRY_FAKER_SEED)%',
3737
service('.zenstruck_foundry.in_memory.repository_registry'),
38+
service('.foundry.persistence.objects_tracker')->nullOnInvalid(),
3839
])
3940
->public()
4041
;

phpstan.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ parameters:
6969
- identifier: missingType.callable
7070
path: tests/Fixture/Maker/expected/
7171

72+
# we're currently running static analysis with PHP 8.3
73+
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::isUninitializedLazyObject\(\).#'
74+
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::resetAsLazyProxy\(\).#'
75+
- message: '#Call to an undefined method ReflectionClass\<(.*)\>::initializeLazyObject\(\).#'
76+
7277
excludePaths:
7378
- tests/Fixture/Maker/expected/can_create_factory_with_auto_activated_not_persisted_option.php
7479
- tests/Fixture/Maker/expected/can_create_factory_interactively.php

phpunit

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
set -o errexit
44
set -o nounset
55

6+
COMPOSER_BIN="php -d xdebug.mode=off $(which composer)"
7+
68
check_phpunit_version() {
7-
INSTALLED_PHPUNIT_VERSION=$(composer info phpunit/phpunit | grep versions | cut -c 14-)
9+
INSTALLED_PHPUNIT_VERSION=$(${COMPOSER_BIN} info phpunit/phpunit | grep versions | cut -c 14-)
810

911
REQUIRED_PHPUNIT_VERSION="${1?}"
1012

@@ -35,7 +37,7 @@ SHOULD_UPDATE_PHPUNIT=$(check_phpunit_version "${PHPUNIT_VERSION}")
3537

3638
if [ "${SHOULD_UPDATE_PHPUNIT}" = "0" ]; then
3739
echo "ℹ️ Upgrading PHPUnit to ${PHPUNIT_VERSION}"
38-
composer update dama/doctrine-test-bundle brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W
40+
${COMPOSER_BIN} update dama/doctrine-test-bundle brianium/paratest "phpunit/phpunit:^${PHPUNIT_VERSION}" -W
3941
fi
4042
### <<
4143

phpunit-deprecation-baseline.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
<line number="25" hash="c6af5d66288d0667e424978000f29571e4954b81">
55
<issue><![CDATA[Since symfony/framework-bundle 6.4: Not setting the "framework.php_errors.log" config option is deprecated. It will default to "true" in 7.0.]]></issue>
66
<issue><![CDATA[Since symfony/var-exporter 7.3: Generating lazy proxy for class "Zenstruck\Foundry\Tests\Integration\ForceFactoriesTraitUsage\SomeObject" is deprecated; leverage native lazy objects instead.]]></issue>
7+
<issue><![CDATA[Since zenstruck/foundry 2.6: Proxy usage is deprecated in PHP 8.4. Use directly PersistentObjectFactory, Foundry now leverages the native PHP lazy system to auto-refresh objects.]]></issue>
78
</line>
89
</file>
910
</files>

src/Configuration.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Zenstruck\Foundry\InMemory\CannotEnableInMemory;
2020
use Zenstruck\Foundry\InMemory\InMemoryRepositoryRegistry;
2121
use Zenstruck\Foundry\Persistence\PersistenceManager;
22+
use Zenstruck\Foundry\Persistence\Proxy\PersistedObjectsTracker;
2223

2324
/**
2425
* @author Kevin Bond <kevinbond@gmail.com>
@@ -60,6 +61,7 @@ public function __construct(
6061
public readonly bool $flushOnce = false,
6162
?int $forcedFakerSeed = null,
6263
public readonly ?InMemoryRepositoryRegistry $inMemoryRepositoryRegistry = null,
64+
public readonly ?PersistedObjectsTracker $persistedObjectsTracker = null,
6365
) {
6466
if (null === self::$instance) {
6567
$this->faker->seed(self::fakerSeed($forcedFakerSeed));

src/Persistence/PersistenceManager.php

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
use Zenstruck\Foundry\Persistence\Relationship\RelationshipMetadata;
2323
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
2424

25+
use function Zenstruck\Foundry\set;
26+
2527
/**
2628
* @author Kevin Bond <kevinbond@gmail.com>
2729
*
@@ -147,7 +149,7 @@ public function flush(ObjectManager $om): void
147149
*
148150
* @return T
149151
*/
150-
public function refresh(object &$object, bool $force = false): object
152+
public function refresh(object &$object, bool $force = false, bool $allowRefreshDeletedObject = false): object
151153
{
152154
if (!$this->flush && !$force) {
153155
return $object;
@@ -157,6 +159,11 @@ public function refresh(object &$object, bool $force = false): object
157159
return $object->_refresh();
158160
}
159161

162+
if (\PHP_VERSION_ID >= 80400 && ($reflector = new \ReflectionClass($object))->isUninitializedLazyObject($object)) {
163+
/** @var T $object */
164+
$object = $reflector->initializeLazyObject($object);
165+
}
166+
160167
$strategy = $this->strategyFor($object::class);
161168

162169
if ($strategy->hasChanges($object)) {
@@ -165,33 +172,49 @@ public function refresh(object &$object, bool $force = false): object
165172

166173
$om = $strategy->objectManagerFor($object::class);
167174

168-
if ($strategy->contains($object)) {
169-
try {
170-
$om->refresh($object);
171-
} catch (\LogicException|\Error) {
172-
// prevent entities/documents with readonly properties to create an error
173-
// LogicException is for ORM / Error is for ODM
174-
// @see https://github.com/doctrine/orm/issues/9505
175-
}
176-
177-
return $object;
178-
}
179-
180175
if ($strategy->isEmbeddable($object)) {
181176
return $object;
182177
}
183178

184-
$id = $om->getClassMetadata($object::class)->getIdentifierValues($object);
179+
if (!$strategy->contains($object)) {
180+
$objectFromDb = null;
185181

186-
if (!$id || !($object = $om->find($object::class, $id))) { // @phpstan-ignore parameterByRef.type
187-
throw RefreshObjectFailed::objectNoLongExists();
182+
$id = $om->getClassMetadata($object::class)->getIdentifierValues($object);
183+
184+
if ($id) {
185+
// "merge" object if it is not managed
186+
$objectFromDb = $om->find($object::class, $id);
187+
}
188+
189+
if ($objectFromDb) {
190+
$object = $objectFromDb;
191+
} else {
192+
if ($allowRefreshDeletedObject) {
193+
return $object;
194+
}
195+
196+
throw RefreshObjectFailed::objectNoLongExists();
197+
}
198+
}
199+
200+
try {
201+
$om->refresh($object);
202+
} catch (\LogicException|\Error) {
203+
// prevent entities/documents with readonly properties to create an error
204+
// LogicException is for ORM / Error is for ODM
205+
// @see https://github.com/doctrine/orm/issues/9505
188206
}
189207

190208
return $object;
191209
}
192210

193211
public function isPersisted(object $object): bool
194212
{
213+
if (\PHP_VERSION_ID >= 80400 && ($reflector = new \ReflectionClass($object))->isUninitializedLazyObject($object)) {
214+
/** @var object $object */
215+
$object = $reflector->initializeLazyObject($object);
216+
}
217+
195218
// prevents doctrine to use its cache and think the object is persisted
196219
if ($this->strategyFor($object::class)->isScheduledForInsert($object)) {
197220
return false;

src/Persistence/PersistentObjectFactory.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,10 @@ static function(object $object, array $parameters, PersistentObjectFactory $fact
483483
return;
484484
}
485485

486+
if (\PHP_VERSION_ID >= 80400 && !$factoryUsed instanceof PersistentProxyObjectFactory) {
487+
Configuration::instance()->persistedObjectsTracker?->add($object);
488+
}
489+
486490
$afterPersistCallbacks = [];
487491

488492
foreach ($factoryUsed->afterPersist as $afterPersist) {

src/Persistence/PersistentProxyObjectFactory.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@
2222
*/
2323
abstract class PersistentProxyObjectFactory extends PersistentObjectFactory
2424
{
25+
public function __construct()
26+
{
27+
parent::__construct();
28+
29+
if (\PHP_VERSION_ID >= 80400) {
30+
trigger_deprecation(
31+
'zenstruck/foundry',
32+
'2.6',
33+
'Proxy usage is deprecated in PHP 8.4. Use directly PersistentObjectFactory, Foundry now leverages the native PHP lazy system to auto-refresh objects.',
34+
);
35+
}
36+
}
37+
2538
/**
2639
* @return class-string<T>
2740
*/
@@ -151,6 +164,6 @@ final public static function repository(): ObjectRepository
151164
{
152165
Configuration::instance()->assertPersistenceEnabled();
153166

154-
return new ProxyRepositoryDecorator(static::class(), Configuration::instance()->isInMemoryEnabled()); // @phpstan-ignore argument.type, return.type
167+
return new ProxyRepositoryDecorator(static::class(), Configuration::instance()->isInMemoryEnabled()); // @phpstan-ignore return.type
155168
}
156169
}

0 commit comments

Comments
 (0)