Skip to content

Commit 373d1e4

Browse files
committed
feat: create proxy system with PHP 8.4 lazy proxies
1 parent b0a4076 commit 373d1e4

File tree

10 files changed

+111
-4
lines changed

10 files changed

+111
-4
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ jobs:
329329
- name: Setup PHP
330330
uses: shivammathur/setup-php@v2
331331
with:
332-
php-version: 8.3
332+
php-version: 8.4
333333
coverage: none
334334

335335
- name: Install dependencies

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,18 @@
3232
"doctrine/common": "^3.2.2",
3333
"doctrine/doctrine-bundle": "^2.10",
3434
"doctrine/doctrine-migrations-bundle": "^2.2|^3.0",
35-
"doctrine/persistence": "^2.0|^3.0|^4.0",
36-
"doctrine/mongodb-odm-bundle": "^4.6|^5.0",
3735
"doctrine/mongodb-odm": "^2.4",
36+
"doctrine/mongodb-odm-bundle": "^4.6|^5.0",
3837
"doctrine/orm": "^2.16|^3.0",
38+
"doctrine/persistence": "^2.0|^3.0|^4.0",
3939
"phpunit/phpunit": "^9.5.0 || ^10.0 || ^11.0 || ^12.0",
40+
"symfony/browser-kit": "^7.2",
4041
"symfony/console": "^6.4|^7.0",
4142
"symfony/dotenv": "^6.4|^7.0",
4243
"symfony/framework-bundle": "^6.4|^7.0",
4344
"symfony/maker-bundle": "^1.55",
4445
"symfony/phpunit-bridge": "^6.4|^7.0",
46+
"symfony/routing": "^7.2",
4547
"symfony/runtime": "^6.4|^7.0",
4648
"symfony/translation-contracts": "^3.4",
4749
"symfony/uid": "^6.4|^7.0",

config/persistence.php

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

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

5+
use Symfony\Component\HttpKernel\Event\TerminateEvent;
56
use Zenstruck\Foundry\Persistence\PersistenceManager;
7+
use Zenstruck\Foundry\Persistence\Proxy\KernelTerminateListener;
68
use Zenstruck\Foundry\Persistence\ResetDatabase\ResetDatabaseManager;
79

810
return static function (ContainerConfigurator $container): void {
@@ -18,4 +20,10 @@
1820
tagged_iterator('.foundry.persistence.schema_resetter'),
1921
])
2022
;
23+
24+
if (PHP_VERSION_ID >= 80400) {
25+
$container->services()->set('.foundry.proxy.kernel_terminate_listener', KernelTerminateListener::class)
26+
->tag('kernel.event_listener', ['event' => TerminateEvent::class, 'method' => '__invoke'])
27+
;
28+
}
2129
};

src/Persistence/PersistentObjectFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
use Zenstruck\Foundry\Persistence\Relationship\OneToManyRelationship;
2828
use Zenstruck\Foundry\Persistence\Relationship\OneToOneRelationship;
2929

30+
use Zenstruck\Foundry\Persistence\Proxy\CreatedObjectsTracker;
31+
3032
use function Zenstruck\Foundry\force;
3133
use function Zenstruck\Foundry\get;
3234
use function Zenstruck\Foundry\set;
@@ -454,6 +456,10 @@ final protected function initializeInternal(): static
454456
return parent::initializeInternal()
455457
->afterInstantiate(
456458
static function(object $object, array $parameters, PersistentObjectFactory $factoryUsed): void {
459+
if (PHP_VERSION_ID >= 80400) {
460+
CreatedObjectsTracker::add($object);
461+
}
462+
457463
if (!$factoryUsed->isPersisting()) {
458464
return;
459465
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Persistence\Proxy;
4+
5+
use WeakReference;
6+
7+
use function Zenstruck\Foundry\Persistence\refresh;
8+
9+
final class CreatedObjectsTracker
10+
{
11+
/** @var list<\WeakReference<object>> */
12+
private static $buffer = [];
13+
14+
public static function add(object $object): void
15+
{
16+
self::$buffer[] = \WeakReference::create($object);
17+
}
18+
19+
public static function proxifyObjects(): void
20+
{
21+
self::$buffer = array_values(
22+
array_map(
23+
static function (WeakReference $weakRef) {
24+
$object = $weakRef->get() ?? throw new \LogicException('Object cannot be null.');
25+
$clone = clone $object;
26+
(new \ReflectionClass($object))->resetAsLazyProxy($object, fn() => refresh($clone));
27+
28+
return \WeakReference::create($object);
29+
},
30+
array_filter(self::$buffer, static fn (WeakReference $weakRef) => $weakRef->get() !== null),
31+
)
32+
);
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Zenstruck\Foundry\Persistence\Proxy;
4+
5+
use Symfony\Component\HttpKernel\Event\TerminateEvent;
6+
7+
final class KernelTerminateListener
8+
{
9+
public function __invoke(TerminateEvent $event): void
10+
{
11+
CreatedObjectsTracker::proxifyObjects();
12+
}
13+
}

tests/Fixture/TestKernel.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@
1111

1212
namespace Zenstruck\Foundry\Tests\Fixture;
1313

14+
use Doctrine\ORM\EntityManagerInterface;
1415
use Symfony\Bundle\MakerBundle\MakerBundle;
1516
use Symfony\Component\Config\Loader\LoaderInterface;
1617
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\HttpFoundation\Response;
19+
use Symfony\Component\Routing\Attribute\Route;
1720
use Zenstruck\Foundry\ORM\ResetDatabase\ResetDatabaseMode;
21+
use Zenstruck\Foundry\Tests\Fixture\Entity\Address;
22+
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
1823
use Zenstruck\Foundry\Tests\Fixture\Factories\ArrayFactory;
1924
use Zenstruck\Foundry\Tests\Fixture\Factories\Object1Factory;
2025
use Zenstruck\Foundry\Tests\Fixture\InMemory\InMemoryAddressRepository;
@@ -56,4 +61,14 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load
5661
$c->register(InMemoryAddressRepository::class)->setAutowired(true)->setAutoconfigured(true);
5762
$c->register(InMemoryContactRepository::class)->setAutowired(true)->setAutoconfigured(true);
5863
}
64+
65+
#[Route('/update/{id}', name: 'test')]
66+
public function __invoke(EntityManagerInterface $entityManager, int $id): Response
67+
{
68+
$genericEntity = $entityManager->find(GenericEntity::class, $id);
69+
$genericEntity?->setProp1('foo');
70+
$entityManager->flush();
71+
72+
return new Response();
73+
}
5974
}

tests/Integration/ORM/GenericEntityFactoryTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111

1212
namespace Zenstruck\Foundry\Tests\Integration\ORM;
1313

14+
use Doctrine\ORM\EntityManagerInterface;
15+
use PHPUnit\Framework\Attributes\RequiresPhp;
1416
use PHPUnit\Framework\Attributes\Test;
17+
use Zenstruck\Foundry\Persistence\Proxy\CreatedObjectsTracker;
18+
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
1519
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\EmptyConstructorFactory;
1620
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericEntityFactory;
1721
use Zenstruck\Foundry\Tests\Integration\Persistence\GenericFactoryTestCase;
@@ -27,6 +31,28 @@ final class GenericEntityFactoryTest extends GenericFactoryTestCase
2731
{
2832
use RequiresORM;
2933

34+
/**
35+
* @test
36+
* @requires PHP >= 8.4
37+
*/
38+
#[Test]
39+
#[RequiresPhp('>= 8.4')]
40+
public function it_can_refresh_objects_with_php84_proxies(): void
41+
{
42+
$object = $this->factory()->create();
43+
self::ensureKernelShutdown();
44+
45+
self::assertSame('default1', $object->getProp1());
46+
self::assertFalse((new \ReflectionClass($object))->isUninitializedLazyObject($object));
47+
48+
$client = self::createClient();
49+
$client->request('GET', "/update/{$object->id}");
50+
51+
self::assertTrue((new \ReflectionClass($object))->isUninitializedLazyObject($object));
52+
self::assertSame('foo', $object->getProp1());
53+
self::assertFalse((new \ReflectionClass($object))->isUninitializedLazyObject($object));
54+
}
55+
3056
/**
3157
* @test
3258
*/

tests/Integration/ORM/GenericEntityProxyFactoryTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111

1212
namespace Zenstruck\Foundry\Tests\Integration\ORM;
1313

14+
use Doctrine\ORM\EntityManagerInterface;
1415
use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
1516
use Zenstruck\Foundry\Tests\Fixture\Entity\EdgeCases\EntityWithReadonly\EntityWithReadonly;
17+
use Zenstruck\Foundry\Tests\Fixture\Entity\GenericEntity;
1618
use Zenstruck\Foundry\Tests\Fixture\Factories\Entity\GenericProxyEntityFactory;
1719
use Zenstruck\Foundry\Tests\Integration\Persistence\GenericProxyFactoryTestCase;
1820
use Zenstruck\Foundry\Tests\Integration\RequiresORM;

tests/Integration/Persistence/GenericFactoryTestCase.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\Attributes\Depends;
1515
use PHPUnit\Framework\Attributes\Test;
1616
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
17+
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
1718
use Zenstruck\Foundry\Configuration;
1819
use Zenstruck\Foundry\Exception\PersistenceDisabled;
1920
use Zenstruck\Foundry\Object\Instantiator;
@@ -37,7 +38,7 @@
3738
/**
3839
* @author Kevin Bond <[email protected]>
3940
*/
40-
abstract class GenericFactoryTestCase extends KernelTestCase
41+
abstract class GenericFactoryTestCase extends WebTestCase
4142
{
4243
use Factories, ResetDatabase;
4344

0 commit comments

Comments
 (0)