Repository: symfony/ux
Pull request #2079 (2024-08-17 22:40:00)

[LiveComponent] Fix PHP8.4 errors + deprecations
First patch of fixes (thanks to the PHP 8.4 tesst)

smnandre: 🆘  @kbond  😅 
smnandre: @kbond sorry i was unclear 😓 

I started on PHP 8.4 but there were deprecations on PHP 8.3.. 

So i decided to un-lock the Foundry version.

This PR failures come from me upgrading Foundry, but are on the current PHP version... and i'm a bit blocked here :/ 

i must have messed something in the upgrade 
nikophil: Hi @smnandre 

I've opened a PR based on yours which fixes the CI

https://github.com/smnandre/ux/pull/2

I'm not really sure where the problem was coming from, though :sweat_smile: 
smnandre: Oh this is no nice, thank you !! 😃 

smnandre: I'll merge it in mine and we'll see from there..  as the CI does not run on my fork :) 
smnandre: Looks good....  (thank you very much!)) .. 
Except Icon now stuck in something that looks to me related to ConsoleTest /  Foundry  !

I'm sorry 😅 
smnandre: Update: not at all, 100% my fault .... 

```
Runtime:       PHP 8.3.10
Configuration: /Users/simonandre/Sites/symfony/ux/src/Icons/phpunit.xml.dist
Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

Testing 
....."""
 ------------ ------- --------- ------------ --------------------- \n
  Icon set     Icons   License   Prefix       Example              \n
 ------------ ------- --------- ------------ --------------------- \n
  Lucide        1524   ISC       lucide       lucide:check-circle  \n
  Lucide Lab     373   ISC       lucide-lab   lucide-lab:venn      \n
 ------------ ------- --------- ------------ --------------------- \n
\n

```


Lucide is just no the only result for the query "lucide" ...  deepest apologies 💐 
kbond: Oi, yeah that's a brittle test
smnandre: Yeah perfect! Thank you @kbond & @nikophil for the help :) 

Patch:
diff --git a/src/LiveComponent/composer.json b/src/LiveComponent/composer.json
index c9b2898c..4647b494 100644
--- a/src/LiveComponent/composer.json
+++ b/src/LiveComponent/composer.json
@@ -51,7 +51,7 @@
         "symfony/twig-bundle": "^5.4|^6.0|^7.0",
         "symfony/validator": "^5.4|^6.0|^7.0",
         "zenstruck/browser": "^1.2.0",
-        "zenstruck/foundry": "1.37.*"
+        "zenstruck/foundry": "^2.0"
     },
     "conflict": {
         "symfony/config": "<5.4.0"
diff --git a/src/LiveComponent/phpunit.xml.dist b/src/LiveComponent/phpunit.xml.dist
index 87b89d58..ca2df4fe 100644
--- a/src/LiveComponent/phpunit.xml.dist
+++ b/src/LiveComponent/phpunit.xml.dist
@@ -18,7 +18,7 @@
         <server name="KERNEL_CLASS" value="Symfony\UX\LiveComponent\Tests\Fixtures\Kernel"/>
         <server name="DATABASE_URL" value="sqlite:///%kernel.project_dir%/var/data.db"/>
         <env name="SHELL_VERBOSITY" value="-1"/>
-        <server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;ignoreFile=./tests/baseline-ignore"/>
+        <server name="SYMFONY_DEPRECATIONS_HELPER" value="max[self]=0&amp;max[direct]=0&amp;quiet[]=indirect&amp;quiet[]=other"/>
     </php>
 
     <testsuites>


Test patch:
diff --git a/src/Icons/tests/Integration/Command/SearchIconCommandTest.php b/src/Icons/tests/Integration/Command/SearchIconCommandTest.php
index d0584250..1e2de704 100644
--- a/src/Icons/tests/Integration/Command/SearchIconCommandTest.php
+++ b/src/Icons/tests/Integration/Command/SearchIconCommandTest.php
@@ -23,19 +23,20 @@ final class SearchIconCommandTest extends KernelTestCase
 
     public function testSearchWithPrefix(): void
     {
-        $this->consoleCommand('ux:icons:search lucide')
+        $this->consoleCommand('ux:icons:search iconoir')
             ->execute()
             ->assertSuccessful()
             ->assertOutputContains('Icon set')
-            ->assertOutputContains('Lucide')
+            ->assertOutputContains('Iconoir')
             ->assertOutputContains('Icons')
             ->assertOutputContains('License')
-            ->assertOutputContains('ISC')
+            ->assertOutputContains('MIT')
             ->assertOutputContains('Prefix')
-            ->assertOutputContains('lucide')
+            ->assertOutputContains('iconoir')
             ->assertOutputContains('Example')
-            ->assertOutputContains('lucide:')
-            ->assertOutputContains('php bin/console ux:icons:search lucide');
+            ->assertOutputContains('iconoir:')
+            ->assertOutputContains('php bin/console ux:icons:search iconoir')
+            ->assertStatusCode(0);
     }
 
     public function testSearchWithPrefixMatchingMultipleSet(): void
diff --git a/src/LiveComponent/tests/Fixtures/Dto/HoldsArrayOfDtos.php b/src/LiveComponent/tests/Fixtures/Dto/HoldsArrayOfDtos.php
index 13eaec51..10d026bf 100644
--- a/src/LiveComponent/tests/Fixtures/Dto/HoldsArrayOfDtos.php
+++ b/src/LiveComponent/tests/Fixtures/Dto/HoldsArrayOfDtos.php
@@ -2,12 +2,21 @@
 
 declare(strict_types=1);
 
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
 namespace Symfony\UX\LiveComponent\Tests\Fixtures\Dto;
 
 final class HoldsArrayOfDtos
 {
     /**
-     * @var Address[] $addresses
+     * @var Address[]
      */
     public array $addresses;
 }
diff --git a/src/LiveComponent/tests/Fixtures/Entity/TodoItemFixtureEntity.php b/src/LiveComponent/tests/Fixtures/Entity/TodoItemFixtureEntity.php
index 33abeb34..0598d7fb 100644
--- a/src/LiveComponent/tests/Fixtures/Entity/TodoItemFixtureEntity.php
+++ b/src/LiveComponent/tests/Fixtures/Entity/TodoItemFixtureEntity.php
@@ -24,15 +24,12 @@ class TodoItemFixtureEntity
     public $id;
 
     #[ORM\Column(type: 'string')]
-    private ?string $name = null;
+    private ?string $name;
 
     #[ORM\ManyToOne(targetEntity: TodoListFixtureEntity::class, inversedBy: 'todoItems')]
     private TodoListFixtureEntity $todoList;
 
-    /**
-     * @param string $name
-     */
-    public function __construct(string $name = null)
+    public function __construct(?string $name = null)
     {
         $this->name = $name;
     }
diff --git a/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php b/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php
index 7cdf882d..dcb3cdc5 100644
--- a/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php
+++ b/src/LiveComponent/tests/Fixtures/Factory/CompositeIdEntityFactory.php
@@ -11,38 +11,20 @@
 
 namespace Symfony\UX\LiveComponent\Tests\Fixtures\Factory;
 
-use Doctrine\ORM\EntityRepository;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\CompositeIdEntity;
-use Zenstruck\Foundry\ModelFactory;
-use Zenstruck\Foundry\Proxy;
-use Zenstruck\Foundry\RepositoryProxy;
+use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
 
 /**
- * @extends ModelFactory<CompositeIdEntity>
- *
- * @method static CompositeIdEntity|Proxy          createOne(array $attributes = [])
- * @method static CompositeIdEntity[]|Proxy[]      createMany(int $number, array|callable $attributes = [])
- * @method static CompositeIdEntity|Proxy          find(object|array|mixed $criteria)
- * @method static CompositeIdEntity|Proxy          findOrCreate(array $attributes)
- * @method static CompositeIdEntity|Proxy          first(string $sortedField = 'id')
- * @method static CompositeIdEntity|Proxy          last(string $sortedField = 'id')
- * @method static CompositeIdEntity|Proxy          random(array $attributes = [])
- * @method static CompositeIdEntity|Proxy          randomOrCreate(array $attributes = []))
- * @method static CompositeIdEntity[]|Proxy[]      all()
- * @method static CompositeIdEntity[]|Proxy[]      findBy(array $attributes)
- * @method static CompositeIdEntity[]|Proxy[]      randomSet(int $number, array $attributes = []))
- * @method static CompositeIdEntity[]|Proxy[]      randomRange(int $min, int $max, array $attributes = []))
- * @method static EntityRepository|RepositoryProxy repository()
- * @method        CompositeIdEntity|Proxy          create(array|callable $attributes = [])
+ * @extends PersistentProxyObjectFactory<CompositeIdEntity>
  */
-class CompositeIdEntityFactory extends ModelFactory
+class CompositeIdEntityFactory extends PersistentProxyObjectFactory
 {
-    protected static function getClass(): string
+    public static function class(): string
     {
         return CompositeIdEntity::class;
     }
 
-    protected function getDefaults(): array
+    protected function defaults(): array
     {
         return [
             'firstIdPart' => rand(1, \PHP_INT_MAX),
diff --git a/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php b/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php
index ba5f9ac5..241eec7e 100644
--- a/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php
+++ b/src/LiveComponent/tests/Fixtures/Factory/ForeignKeyIdEntityFactory.php
@@ -11,41 +11,23 @@
 
 namespace Symfony\UX\LiveComponent\Tests\Fixtures\Factory;
 
-use Doctrine\ORM\EntityRepository;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\Entity1;
 use Symfony\UX\LiveComponent\Tests\Fixtures\Entity\ForeignKeyIdEntity;
-use Zenstruck\Foundry\ModelFactory;
-use Zenstruck\Foundry\Proxy;
-use Zenstruck\Foundry\RepositoryProxy;
+use Zenstruck\Foundry\Persistence\PersistentProxyObjectFactory;
 
 use function Zenstruck\Foundry\lazy;
 
 /**
- * @extends ModelFactory<ForeignKeyIdEntity>
- *
- * @method static ForeignKeyIdEntity|Proxy         createOne(array $attributes = [])
- * @method static ForeignKeyIdEntity[]|Proxy[]     createMany(int $number, array|callable $attributes = [])
- * @method static ForeignKeyIdEntity|Proxy         find(object|array|mixed $criteria)
- * @method static ForeignKeyIdEntity|Proxy         findOrCreate(array $attributes)
- * @method static ForeignKeyIdEntity|Proxy         first(string $sortedField = 'id')
- * @method static ForeignKeyIdEntity|Proxy         last(string $sortedField = 'id')
- * @method static ForeignKeyIdEntity|Proxy         random(array $attributes = [])
- * @method static ForeignKeyIdEntity|Proxy         randomOrCreate(array $attributes = []))
- * @method static ForeignKeyIdEntity[]|Proxy[]     all()
- * @method static ForeignKeyIdEntity[]|Proxy[]     findBy(array $attributes)
- * @method static ForeignKeyIdEntity[]|Proxy[]     randomSet(int $number, array $attributes = []))
- * @method static ForeignKeyIdEntity[]|Proxy[]     randomRange(int $min, int $max, array $attributes = []))
- * @method static EntityRepository|RepositoryProxy repository()
- * @method        ForeignKeyIdEntity|Proxy         create(array|callable $attributes = [])
+ * @extends PersistentProxyObjectFactory<ForeignKeyIdEntity>
  */
-class ForeignKeyIdEntityFactory extends ModelFactory
+class ForeignKeyIdEntityFactory extends PersistentProxyObjectFactory
 {
-    protected static function getClass(): string
+    public static function class(): string
     {
         return ForeignKeyIdEntity::class;
     }
 
-    protected function getDefaults(): array
+    protected function defaults(): array
     {
         return ['id' => lazy(static fn () => new Entity1())];
     }
diff --git a/src/LiveComponent/tests/Fixtures/Kernel.php b/src/LiveComponent/tests/Fixtures/Kernel.php
index b8b4df0b..b7d90187 100644
--- a/src/LiveComponent/tests/Fixtures/Kernel.php
+++ b/src/LiveComponent/tests/Fixtures/Kernel.php
@@ -51,14 +51,14 @@ final class Kernel extends BaseKernel
         return new Response('index');
     }
 
-    public function renderTemplate(string $template, Environment $twig = null): Response
+    public function renderTemplate(string $template, ?Environment $twig = null): Response
     {
         $twig ??= $this->container->get('twig');
 
         return new Response($twig->render("{$template}.html.twig"));
     }
 
-    public function renderNamespacedTemplate(string $template, Environment $twig = null): Response
+    public function renderNamespacedTemplate(string $template, ?Environment $twig = null): Response
     {
         $twig ??= $this->container->get('twig');
 
@@ -171,6 +171,7 @@ final class Kernel extends BaseKernel
                 ],
             ],
         ];
+
         if (null !== $doctrineBundleVersion = InstalledVersions::getVersion('doctrine/doctrine-bundle')) {
             if (version_compare($doctrineBundleVersion, '2.8.0', '>=')) {
                 $doctrineConfig['orm']['enable_lazy_ghost_objects'] = true;
@@ -181,14 +182,13 @@ final class Kernel extends BaseKernel
                 $doctrineConfig['orm']['validate_xml_mapping'] = true;
                 $doctrineConfig['dbal']['schema_manager_factory'] = 'doctrine.dbal.default_schema_manager_factory';
             }
+            if (version_compare($doctrineBundleVersion, '2.12.0', '>=')) {
+                $doctrineConfig['orm']['controller_resolver']['auto_mapping'] = false;
+            }
         }
 
         $c->extension('doctrine', $doctrineConfig);
 
-        $c->extension('zenstruck_foundry', [
-            'auto_refresh_proxies' => false,
-        ]);
-
         $c->services()
             ->defaults()
             ->autowire()
diff --git a/src/LiveComponent/tests/Fixtures/Serializer/Entity2Normalizer.php b/src/LiveComponent/tests/Fixtures/Serializer/Entity2Normalizer.php
index c1b449e4..b63331a3 100644
--- a/src/LiveComponent/tests/Fixtures/Serializer/Entity2Normalizer.php
+++ b/src/LiveComponent/tests/Fixtures/Serializer/Entity2Normalizer.php
@@ -47,7 +47,7 @@ final class Entity2Normalizer implements NormalizerInterface, DenormalizerInterf
         return $data instanceof Entity2;
     }
 
-    public function getSupportedTypes(?string $format): array
+    public function getSupportedTypes(?string $format = null): array
     {
         return [Entity2::class => true];
     }
diff --git a/src/LiveComponent/tests/Fixtures/Serializer/MoneyNormalizer.php b/src/LiveComponent/tests/Fixtures/Serializer/MoneyNormalizer.php
index a666230f..90889a47 100644
--- a/src/LiveComponent/tests/Fixtures/Serializer/MoneyNormalizer.php
+++ b/src/LiveComponent/tests/Fixtures/Serializer/MoneyNormalizer.php
@@ -37,7 +37,7 @@ final class MoneyNormalizer implements NormalizerInterface, DenormalizerInterfac
         return $data instanceof Money;
     }
 
-    public function getSupportedTypes(?string $format): array
+    public function getSupportedTypes(string $format = null): array
     {
         return [Money::class => true];
     }
diff --git a/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php b/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
index dce6db3c..e4a87734 100644
--- a/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
+++ b/src/LiveComponent/tests/Functional/EventListener/LiveComponentSubscriberTest.php
@@ -22,7 +22,7 @@ use Zenstruck\Browser\Test\HasBrowser;
 use Zenstruck\Foundry\Test\Factories;
 use Zenstruck\Foundry\Test\ResetDatabase;
 
-use function Zenstruck\Foundry\create;
+use function Zenstruck\Foundry\Persistence\persist;
 
 /**
  * @author Kevin Bond <kevinbond@gmail.com>
@@ -48,7 +48,7 @@ final class LiveComponentSubscriberTest extends KernelTestCase
     public function testCanRenderComponentAsHtml(): void
     {
         $component = $this->mountComponent('component1', [
-            'prop1' => $entity = create(Entity1::class)->object(),
+            'prop1' => $entity = persist(Entity1::class),
             'prop2' => $date = new \DateTime('2021-03-05 9:23'),
             'prop3' => 'value3',
             'prop4' => 'value4',
diff --git a/src/LiveComponent/tests/Functional/LiveResponderTest.php b/src/LiveComponent/tests/Functional/LiveResponderTest.php
index f47b60d2..8d533164 100644
--- a/src/LiveComponent/tests/Functional/LiveResponderTest.php
+++ b/src/LiveComponent/tests/Functional/LiveResponderTest.php
@@ -9,7 +9,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Symfony\UX\LiveComponent\Tests\Functional\EventListener;
+namespace Symfony\UX\LiveComponent\Tests\Functional;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
diff --git a/src/LiveComponent/tests/Functional/ValidatableComponentTraitTest.php b/src/LiveComponent/tests/Functional/ValidatableComponentTraitTest.php
index 1d59eadd..ce8d02fc 100644
--- a/src/LiveComponent/tests/Functional/ValidatableComponentTraitTest.php
+++ b/src/LiveComponent/tests/Functional/ValidatableComponentTraitTest.php
@@ -9,7 +9,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Symfony\UX\LiveComponent\Tests\Functional\Form;
+namespace Symfony\UX\LiveComponent\Tests\Functional;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\UX\LiveComponent\Tests\LiveComponentTestHelper;
diff --git a/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php b/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php
index 2b5ea237..34b1326a 100644
--- a/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php
+++ b/src/LiveComponent/tests/Integration/EventListener/DataModelPropsSubscriberTest.php
@@ -9,7 +9,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Symfony\UX\LiveComponent\Tests\Integration;
+namespace Symfony\UX\LiveComponent\Tests\Integration\EventListener;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\Component\HttpFoundation\Request;
diff --git a/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php b/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php
index ff7b60ca..1b9de696 100644
--- a/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php
+++ b/src/LiveComponent/tests/Integration/Hydration/DoctrineEntityHydrationExtensionTest.php
@@ -9,7 +9,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Symfony\UX\LiveComponent\Tests\Unit\Hydration;
+namespace Symfony\UX\LiveComponent\Tests\Integration\Hydration;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\UX\LiveComponent\Hydration\DoctrineEntityHydrationExtension;
@@ -27,7 +27,7 @@ class DoctrineEntityHydrationExtensionTest extends KernelTestCase
 
     public function testCompositeId(): void
     {
-        $compositeIdEntity = CompositeIdEntityFactory::createOne()->save()->object();
+        $compositeIdEntity = CompositeIdEntityFactory::createOne()->_real();
 
         /** @var DoctrineEntityHydrationExtension $extension */
         $extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension');
@@ -40,7 +40,7 @@ class DoctrineEntityHydrationExtensionTest extends KernelTestCase
 
     public function testForeignKeyId(): void
     {
-        $foreignKeyIdEntity = ForeignKeyIdEntityFactory::createOne()->save()->object();
+        $foreignKeyIdEntity = ForeignKeyIdEntityFactory::createOne()->_real();
 
         /** @var DoctrineEntityHydrationExtension $extension */
         $extension = self::getContainer()->get('ux.live_component.doctrine_entity_hydration_extension');
diff --git a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
index d29a1ab4..511a5580 100644
--- a/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
+++ b/src/LiveComponent/tests/Integration/LiveComponentHydratorTest.php
@@ -44,7 +44,9 @@ use Symfony\UX\TwigComponent\ComponentMetadata;
 use Zenstruck\Foundry\Test\Factories;
 use Zenstruck\Foundry\Test\ResetDatabase;
 
-use function Zenstruck\Foundry\create;
+use function Zenstruck\Foundry\object;
+use function Zenstruck\Foundry\Persistence\persist;
+use function Zenstruck\Foundry\Persistence\proxy;
 
 /**
  * @author Kevin Bond <kevinbond@gmail.com>
@@ -61,7 +63,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
             $this->markTestSkipped(\sprintf('Test requires PHP version %s or higher.', $minPhpVersion));
         }
 
-        // lazily create the test case so each case can prep its data with an isolated container
+        // lazily persist the test case so each case can prep its data with an isolated container
         $testBuilder = $testFactory();
         if (!$testBuilder instanceof HydrationTest) {
             throw new \InvalidArgumentException('Test case callable must return a HydrationTest instance.');
@@ -77,7 +79,6 @@ final class LiveComponentHydratorTest extends KernelTestCase
         $hydratedComponent2 = clone $testCase->component;
 
         $liveMetadata = $testCase->liveMetadata;
-
         $this->factory()->mountFromObject(
             $originalComponentWithData,
             $testCase->inputProps,
@@ -180,9 +181,9 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'onUpdated: set to an array' => [function () {
-            $product = create(ProductFixtureEntity::class, [
+            $product = persist(ProductFixtureEntity::class, [
                 'name' => 'Chicken',
-            ])->object();
+            ]);
 
             return HydrationTest::create(new class {
                 #[LiveProp(writable: ['name'], onUpdated: ['name' => 'onNameUpdated'])]
@@ -203,8 +204,8 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'onUpdated: with IDENTITY' => [function () {
-            $entityOriginal = create(Entity1::class)->object();
-            $entityNext = create(Entity1::class)->object();
+            $entityOriginal = persist(Entity1::class);
+            $entityNext = persist(Entity1::class);
             \assert($entityOriginal instanceof Entity1);
             \assert($entityNext instanceof Entity1);
 
@@ -236,7 +237,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'string: (de)hydrates correctly' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public string $firstName;
             })
                 ->mountWith(['firstName' => 'Ryan'])
@@ -248,7 +249,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'string: changing non-writable causes checksum fail' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public string $firstName;
             })
                 ->mountWith(['firstName' => 'Ryan'])
@@ -294,7 +295,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
             $date = new \DateTime('2023-03-05 9:23', new \DateTimeZone('America/New_York'));
 
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public \DateTime $createdAt;
             })
                 ->mountWith(['createdAt' => $date])
@@ -309,11 +310,11 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Persisted entity: (de)hydration works correctly to/from id' => [function () {
-            $entity1 = create(Entity1::class)->object();
+            $entity1 = persist(Entity1::class);
             \assert($entity1 instanceof Entity1);
 
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public Entity1 $entity1;
             })
                 ->mountWith(['entity1' => $entity1])
@@ -328,8 +329,8 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Persisted entity: writable CAN be changed via id' => [function () {
-            $entityOriginal = create(Entity1::class)->object();
-            $entityNext = create(Entity1::class)->object();
+            $entityOriginal = persist(Entity1::class);
+            $entityNext = persist(Entity1::class);
             \assert($entityOriginal instanceof Entity1);
             \assert($entityNext instanceof Entity1);
 
@@ -349,8 +350,8 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Persisted entity: writable (via IDENTITY constant) CAN be changed via id' => [function () {
-            $entityOriginal = create(Entity1::class)->object();
-            $entityNext = create(Entity1::class)->object();
+            $entityOriginal = persist(Entity1::class);
+            $entityNext = persist(Entity1::class);
             \assert($entityOriginal instanceof Entity1);
             \assert($entityNext instanceof Entity1);
 
@@ -370,9 +371,9 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Persisted entity: non-writable identity but with writable paths updates correctly' => [function () {
-            $product = create(ProductFixtureEntity::class, [
+            $product = persist(ProductFixtureEntity::class, [
                 'name' => 'Rubber Chicken',
-            ])->object();
+            ]);
 
             return HydrationTest::create(new class {
                 #[LiveProp(writable: ['name'])]
@@ -400,16 +401,16 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Persisted entity: deleting entity between dehydration and hydration sets it to null' => [function () {
-            $product = create(ProductFixtureEntity::class);
+            $product = proxy(persist(ProductFixtureEntity::class));
 
             return HydrationTest::create(new class {
                 // test that event the writable path doesn't cause problems
                 #[LiveProp(writable: ['name'])]
                 public ?ProductFixtureEntity $product;
             })
-                ->mountWith(['product' => $product->object()])
+                ->mountWith(['product' => $product->_real()])
                 ->beforeHydration(function () use ($product) {
-                    $product->remove();
+                    $product->_delete();
                 })
                 ->assertObjectAfterHydration(function (object $object) {
                     self::assertNull(
@@ -420,7 +421,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Persisted entity: with custom_normalizer and embeddable (de)hydrates correctly' => [function () {
-            $entity2 = create(Entity2::class, ['embedded1' => new Embeddable1('bar'), 'embedded2' => new Embeddable2('baz')])->object();
+            $entity2 = persist(Entity2::class, ['embedded1' => new Embeddable1('bar'), 'embedded2' => new Embeddable2('baz')]);
 
             return HydrationTest::create(new class {
                 #[LiveProp(useSerializerForHydration: true)]
@@ -470,7 +471,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'Index array: (de)hydrates correctly' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public array $foods = [];
             })
                 ->mountWith(['foods' => ['banana', 'popcorn']])
@@ -504,7 +505,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'Associative array: (de)hyrates correctly' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public array $options = [];
             })
                 ->mountWith(['options' => [
@@ -733,7 +734,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'Empty array: (de)hydrates correctly' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public array $foods = [];
             })
                 ->mountWith([])
@@ -748,9 +749,9 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Array with objects: (de)hydrates correctly' => [function () {
-            $prod1 = create(ProductFixtureEntity::class, ['name' => 'item1'])->object();
+            $prod1 = persist(ProductFixtureEntity::class, ['name' => 'item1']);
             $prod2 = new ProductFixtureEntity();
-            $prod3 = create(ProductFixtureEntity::class, ['name' => 'item3'])->object();
+            $prod3 = persist(ProductFixtureEntity::class, ['name' => 'item3']);
 
             return HydrationTest::create(new DummyObjectWithObjects())
                 ->mountWith(['products' => [$prod1, $prod2, $prod3]])
@@ -778,10 +779,10 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'Enum: null remains null' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public ?IntEnum $int = null;
 
-                #[LiveProp()]
+                #[LiveProp]
                 public ?StringEnum $string = null;
             })
                 ->mountWith([])
@@ -795,10 +796,10 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'Enum: (de)hydrates correctly' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public ?IntEnum $int = null;
 
-                #[LiveProp()]
+                #[LiveProp]
                 public ?StringEnum $string = null;
             })
                 ->mountWith(['int' => IntEnum::HIGH, 'string' => StringEnum::ACTIVE])
@@ -970,14 +971,14 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'Array with DTOs: fully writable allows anything to change' => [function () {
-            $address1 = create(Address::class, ['address' => '17 Arcadia Road', 'city' => 'London'])->object();
-            $address2 = create(Address::class, ['address' => '4 Privet Drive', 'city' => 'Little Whinging'])->object();
-            $address3 = create(Address::class, ['address' => '124 Conch St.', 'city' => 'Bikini Bottom'])->object();
-            $address4 = create(Address::class, ['address' => '32 Windsor Gardens', 'city' => 'London'])->object();
+            $address1 = object(Address::class, ['address' => '17 Arcadia Road', 'city' => 'London']);
+            $address2 = object(Address::class, ['address' => '4 Privet Drive', 'city' => 'Little Whinging']);
+            $address3 = object(Address::class, ['address' => '124 Conch St.', 'city' => 'Bikini Bottom']);
+            $address4 = object(Address::class, ['address' => '32 Windsor Gardens', 'city' => 'London']);
 
             return HydrationTest::create(new class {
                 /**
-                 * @var Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address[]
+                 * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address[]
                  */
                 #[LiveProp(writable: true, useSerializerForHydration: true)]
                 public array $addresses = [];
@@ -996,19 +997,19 @@ final class LiveComponentHydratorTest extends KernelTestCase
                 ->userUpdatesProps(['addresses' => [$address3, $address4]])
                 ->assertObjectAfterHydration(function (object $object) {
                     self::assertEquals([
-                        create(Address::class, ['address' => '124 Conch St.', 'city' => 'Bikini Bottom'])->object(),
-                        create(Address::class, ['address' => '32 Windsor Gardens', 'city' => 'London'])->object(),
+                        object(Address::class, ['address' => '124 Conch St.', 'city' => 'Bikini Bottom']),
+                        object(Address::class, ['address' => '32 Windsor Gardens', 'city' => 'London']),
                     ], $object->addresses);
                 });
         }];
 
         yield 'Array with DTOs: fully writable allows partial changes' => [function () {
-            $address1 = create(Address::class, ['address' => '1600 Pennsylvania Avenue', 'city' => 'Washington DC'])->object();
-            $address2 = create(Address::class, ['address' => '221 B Baker St', 'city' => 'Birmingham'])->object();
+            $address1 = object(Address::class, ['address' => '1600 Pennsylvania Avenue', 'city' => 'Washington DC']);
+            $address2 = object(Address::class, ['address' => '221 B Baker St', 'city' => 'Birmingham']);
 
             return HydrationTest::create(new class {
                 /**
-                 * @var Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address[]
+                 * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\Address[]
                  */
                 #[LiveProp(writable: true, useSerializerForHydration: true)]
                 public array $addresses = [];
@@ -1027,8 +1028,8 @@ final class LiveComponentHydratorTest extends KernelTestCase
                 ->userUpdatesProps(['addresses.1.city' => 'London'])
                 ->assertObjectAfterHydration(function (object $object) {
                     self::assertEquals([
-                        create(Address::class, ['address' => '1600 Pennsylvania Avenue', 'city' => 'Washington DC'])->object(),
-                        create(Address::class, ['address' => '221 B Baker St', 'city' => 'London'])->object(),
+                        object(Address::class, ['address' => '1600 Pennsylvania Avenue', 'city' => 'Washington DC']),
+                        object(Address::class, ['address' => '221 B Baker St', 'city' => 'London']),
                     ], $object->addresses);
                 });
         }];
@@ -1036,21 +1037,21 @@ final class LiveComponentHydratorTest extends KernelTestCase
         yield 'Array with DTOs: fully writable allows deep partial changes' => [function () {
             return HydrationTest::create(new class {
                 /**
-                 * @var Symfony\UX\LiveComponent\Tests\Fixtures\Dto\HoldsArrayOfDtos[]
+                 * @var \Symfony\UX\LiveComponent\Tests\Fixtures\Dto\HoldsArrayOfDtos[] $dtos
                  */
                 #[LiveProp(writable: true, useSerializerForHydration: true)]
                 public array $dtos = [];
             })
                 ->mountWith(['dtos' => [
-                    create(HoldsArrayOfDtos::class, ['addresses' => [
-                        create(Address::class, ['address' => '742 Evergreen Terrace', 'city' => 'Boston'])->object(),
-                        create(Address::class, ['address' => 'Apartment 5A, 129 West 81st Street', 'city' => 'New York'])->object(),
-                        create(Address::class, ['address' => '52 Festive Road', 'city' => 'London'])->object(),
-                    ]])->object(),
-                    create(HoldsArrayOfDtos::class, ['addresses' => [
-                        create(Address::class, ['address' => '698 Sycamore Road', 'city' => 'San Pueblo'])->object(),
-                        create(Address::class, ['address' => 'Madison Square Garden', 'city' => 'Chicago'])->object(),
-                    ]])->object(),
+                    object(HoldsArrayOfDtos::class, ['addresses' => [
+                        object(Address::class, ['address' => '742 Evergreen Terrace', 'city' => 'Boston']),
+                        object(Address::class, ['address' => 'Apartment 5A, 129 West 81st Street', 'city' => 'New York']),
+                        object(Address::class, ['address' => '52 Festive Road', 'city' => 'London']),
+                    ]]),
+                    object(HoldsArrayOfDtos::class, ['addresses' => [
+                        object(Address::class, ['address' => '698 Sycamore Road', 'city' => 'San Pueblo']),
+                        object(Address::class, ['address' => 'Madison Square Garden', 'city' => 'Chicago']),
+                    ]]),
                 ]])
                 ->assertDehydratesTo(['dtos' => [
                     [
@@ -1070,20 +1071,20 @@ final class LiveComponentHydratorTest extends KernelTestCase
                 ->userUpdatesProps([
                     'dtos.0.addresses.0.city' => 'Springfield',
                     'dtos.1.addresses.1.address' => '1060 West Addison Street',
-                    'dtos.1.addresses.1' => create(Address::class, ['address' => '10 Downing Street', 'city' => 'London'])->object(),
+                    'dtos.1.addresses.1' => object(Address::class, ['address' => '10 Downing Street', 'city' => 'London']),
                 ])
                 ->assertObjectAfterHydration(function (object $object) {
                     self::assertEquals(
                         [
-                            create(HoldsArrayOfDtos::class, ['addresses' => [
-                                create(Address::class, ['address' => '742 Evergreen Terrace', 'city' => 'Springfield'])->object(),
-                                create(Address::class, ['address' => 'Apartment 5A, 129 West 81st Street', 'city' => 'New York'])->object(),
-                                create(Address::class, ['address' => '52 Festive Road', 'city' => 'London'])->object(),
-                            ]])->object(),
-                            create(HoldsArrayOfDtos::class, ['addresses' => [
-                                create(Address::class, ['address' => '698 Sycamore Road', 'city' => 'San Pueblo'])->object(),
-                                create(Address::class, ['address' => '10 Downing Street', 'city' => 'London'])->object(),
-                            ]])->object(),
+                            object(HoldsArrayOfDtos::class, ['addresses' => [
+                                object(Address::class, ['address' => '742 Evergreen Terrace', 'city' => 'Springfield']),
+                                object(Address::class, ['address' => 'Apartment 5A, 129 West 81st Street', 'city' => 'New York']),
+                                object(Address::class, ['address' => '52 Festive Road', 'city' => 'London']),
+                            ]]),
+                            object(HoldsArrayOfDtos::class, ['addresses' => [
+                                object(Address::class, ['address' => '698 Sycamore Road', 'city' => 'San Pueblo']),
+                                object(Address::class, ['address' => '10 Downing Street', 'city' => 'London']),
+                            ]]),
                         ],
                         $object->dtos
                     );
@@ -1209,7 +1210,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'Updating non-writable property is rejected' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp()]
+                #[LiveProp]
                 public string $name;
             })
                 ->mountWith(['name' => 'Ryan'])
@@ -1258,7 +1259,7 @@ final class LiveComponentHydratorTest extends KernelTestCase
 
         yield 'It is valid to dehydrate to a fully-writable array' => [function () {
             return HydrationTest::create(new class {
-                #[LiveProp(writable: true, dehydrateWith: 'dehydrateDate', hydrateWith: 'hydrateDate')]
+                #[LiveProp(writable: true, hydrateWith: 'hydrateDate', dehydrateWith: 'dehydrateDate')]
                 public \DateTime $createdAt;
 
                 public function __construct()
@@ -1503,10 +1504,10 @@ final class LiveComponentHydratorTest extends KernelTestCase
         }];
 
         yield 'invalid_types_writable_path_values_not_accepted' => [function () {
-            $product = create(ProductFixtureEntity::class, [
+            $product = persist(ProductFixtureEntity::class, [
                 'name' => 'oranges',
                 'price' => 199,
-            ])->object();
+            ]);
 
             return HydrationTest::create(new class {
                 #[LiveProp(writable: ['name', 'price'])]
@@ -1913,7 +1914,9 @@ class HydrationTestCase
 
 class DummyObjectWithObjects
 {
-    #[LiveProp()]
-    /** @var ProductFixtureEntity[] */
+    /**
+     * @var ProductFixtureEntity[]
+     */
+    #[LiveProp]
     public array $products = [];
 }
diff --git a/src/LiveComponent/tests/Integration/Twig/LiveComponentExtensionTest.php b/src/LiveComponent/tests/Integration/Twig/LiveComponentExtensionTest.php
index 57d253e6..abfd0a1c 100644
--- a/src/LiveComponent/tests/Integration/Twig/LiveComponentExtensionTest.php
+++ b/src/LiveComponent/tests/Integration/Twig/LiveComponentExtensionTest.php
@@ -9,7 +9,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Symfony\UX\LiveComponent\Tests\Integration;
+namespace Symfony\UX\LiveComponent\Tests\Integration\Twig;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 
diff --git a/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php b/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php
index 350ce2bf..1b6884ea 100644
--- a/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php
+++ b/src/LiveComponent/tests/Integration/Twig/LiveComponentRuntimeTest.php
@@ -9,7 +9,7 @@
  * file that was distributed with this source code.
  */
 
-namespace Symfony\UX\LiveComponent\Tests\Integration;
+namespace Symfony\UX\LiveComponent\Tests\Integration\Twig;
 
 use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 use Symfony\UX\LiveComponent\Twig\LiveComponentRuntime;
diff --git a/src/LiveComponent/tests/Integration/Util/FingerprintCalculatorTest.php b/src/LiveComponent/tests/Integration/Util/FingerprintCalculatorTest.php
index 0371ed7c..8a5edd9b 100644
--- a/src/LiveComponent/tests/Integration/Util/FingerprintCalculatorTest.php
+++ b/src/LiveComponent/tests/Integration/Util/FingerprintCalculatorTest.php
@@ -18,7 +18,7 @@ use Symfony\UX\LiveComponent\Util\FingerprintCalculator;
 use Zenstruck\Foundry\Test\Factories;
 use Zenstruck\Foundry\Test\ResetDatabase;
 
-use function Zenstruck\Foundry\create;
+use function Zenstruck\Foundry\Persistence\persist;
 
 final class FingerprintCalculatorTest extends KernelTestCase
 {
@@ -28,7 +28,7 @@ final class FingerprintCalculatorTest extends KernelTestCase
     public function testFingerprintEqual()
     {
         $fingerprintCalculator = $this->getFingerprintCalculator();
-        $entityOne = create(Entity1::class)->object();
+        $entityOne = persist(Entity1::class);
 
         $entityTwo = clone $entityOne;
 
@@ -61,9 +61,9 @@ final class FingerprintCalculatorTest extends KernelTestCase
     {
         $fingerprintCalculator = $this->getFingerprintCalculator();
 
-        $entityOne = create(Entity1::class)->object();
+        $entityOne = persist(Entity1::class);
 
-        $entityTwo = create(Entity1::class)->object();
+        $entityTwo = persist(Entity1::class);
 
         $metadata1 = $this->createMock(LiveComponentMetadata::class);
         $metadata1