From 0948de6b52b31f43a2d582909306e832b37ac977 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Oct 2026 23:57:07 +0000 Subject: [PATCH 1/3] test(bridge): cover associations between woven Doctrine entities Issue #327 reported that under the 3.x extend-based engine the mapping attributes ended up on the renamed parent class (Entity__AopProxied): the table name got the suffix, every association pointed at the parent, joins were built with mismatched aliases and lazy loading was broken. The trait-based engine keeps the mapping on the woven class itself. Add two associated fixture entities (bidirectional one-to-many/many-to-one, every public method advised) and a fixture script that runs them through Doctrine ORM against in-memory SQLite: schema, persist, a DQL join, lazy loading of the many-to-one side as a native lazy object and of the collection. The functional test asserts table names, association targets, the exact SQL and that advices run only for user code. Closes #327 Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018dKKrvU4XVsKSfKRXPpwUS --- src/Core/AGENTS.md | 2 +- .../project/bin/doctrine-associations.php | 122 ++++++++++++++++++ .../project/src/Aspect/EntityFieldAspect.php | 9 ++ tests/Fixtures/project/src/Entity/Sensor.php | 58 +++++++++ .../project/src/Entity/SensorValue.php | 42 ++++++ tests/Functional/DoctrineBridgeTest.php | 118 ++++++++++++++--- 6 files changed, 332 insertions(+), 19 deletions(-) create mode 100644 tests/Fixtures/project/bin/doctrine-associations.php create mode 100644 tests/Fixtures/project/src/Entity/Sensor.php create mode 100644 tests/Fixtures/project/src/Entity/SensorValue.php diff --git a/src/Core/AGENTS.md b/src/Core/AGENTS.md index 81ccc296..ef89860d 100644 --- a/src/Core/AGENTS.md +++ b/src/Core/AGENTS.md @@ -24,4 +24,4 @@ src/Bridge/Doctrine (ORM 3.6+, persistence 4.1+ ClassLocator; no metadata surgery needed under the trait engine): - WovenEntityClassLocator — ClassLocator for mapping drivers: parses class names (parser-reflection ReflectionFile, never includes files) and loads them via class_exists() so the autoloader serves the woven class. Doctrine's FileClassLocator require_once's sources: bypasses weaving / misses already-loaded woven entities. - MetadataLoadInterceptor — loadClassMetadata guard: woven entity (implements Go\Aop\Proxy) with a hooked (intercepted) mapped property while native lazy objects are off → InvalidConfigurationException pointing to enableNativeLazyObjects(true). -- Functional coverage: tests/Functional/DoctrineBridgeTest.php (fixture script tests/Fixtures/project/bin/doctrine-metadata.php, no DB connection needed). +- Functional coverage: tests/Functional/DoctrineBridgeTest.php — fixture scripts tests/Fixtures/project/bin/doctrine-metadata.php (metadata of one woven entity, no DB connection) and doctrine-associations.php (bidirectional association between two woven entities against in-memory SQLite: DQL join, lazy many-to-one and collection; issue #327, skipped without pdo_sqlite). diff --git a/tests/Fixtures/project/bin/doctrine-associations.php b/tests/Fixtures/project/bin/doctrine-associations.php new file mode 100644 index 00000000..6e1e1790 --- /dev/null +++ b/tests/Fixtures/project/bin/doctrine-associations.php @@ -0,0 +1,122 @@ + Executed SQL statements */ + public array $queries = []; + + public function log($level, string|\Stringable $message, array $context = []): void + { + if (is_string($context['sql'] ?? null)) { + $this->queries[] = $context['sql']; + } + } +}; + +$configuration = new Configuration(); +$configuration->setMetadataDriverImpl( + new AttributeDriver(WovenEntityClassLocator::createFromDirectories([__DIR__ . '/../src/Entity'])), +); +$configuration->enableNativeLazyObjects(true); +$configuration->setMiddlewares([new Middleware($logger)]); + +$connection = DriverManager::getConnection(['driver' => 'pdo_sqlite', 'memory' => true], $configuration); +$entityManager = new EntityManager($connection, $configuration); +$entityManager->getEventManager()->addEventListener(Events::loadClassMetadata, new MetadataLoadInterceptor()); + +$sensorMetadata = $entityManager->getClassMetadata(Sensor::class); +$valueMetadata = $entityManager->getClassMetadata(SensorValue::class); +new SchemaTool($entityManager)->createSchema([$sensorMetadata, $valueMetadata]); + +ob_start(); +$sensor = new Sensor('thermometer'); +$sensor->addValue(20); +$sensor->addValue(25); +$entityManager->persist($sensor); +$entityManager->flush(); +$sensorId = $sensor->getId(); +$valueIds = array_map(static fn(SensorValue $value): ?int => $value->getId(), $sensor->getValues()->toArray()); +$entityManager->clear(); +$persistOutput = ob_get_clean(); + +// DQL join over the association, both sides hydrated at once +ob_start(); +$joined = $entityManager->createQuery( + 'SELECT s, v FROM ' . Sensor::class . ' s JOIN s.values v WHERE s.id = :id ORDER BY v.value ASC', +)->setParameter('id', $sensorId)->getResult(); +assert(is_array($joined) && count($joined) === 1 && $joined[0] instanceof Sensor); +$joinedSensor = $joined[0]; +$joinedValues = array_map(static fn(SensorValue $value): int => $value->getValue(), $joinedSensor->getValues()->toArray()); +$entityManager->clear(); +$joinOutput = ob_get_clean(); + +// Lazy loading of the many-to-one side: a native lazy ghost of the woven class +ob_start(); +$value = $entityManager->find(SensorValue::class, $valueIds[0]); +assert($value instanceof SensorValue); +$lazySensor = $value->getSensor(); +$lazySensorClass = $lazySensor::class; +$lazySensorUninitialized = $entityManager->isUninitializedObject($lazySensor); +$lazySensorName = $lazySensor->getName(); +$lazySensorInitialized = !$entityManager->isUninitializedObject($lazySensor); +$entityManager->clear(); +$lazyOutput = ob_get_clean(); + +// Lazy loading of the collection side +$queriesBeforeCollection = count($logger->queries); +ob_start(); +$foundSensor = $entityManager->find(Sensor::class, $sensorId); +assert($foundSensor instanceof Sensor); +$collectionCount = $foundSensor->getValues()->count(); +$collectionOutput = ob_get_clean(); +$collectionLoadQueries = array_slice($logger->queries, $queriesBeforeCollection); + +echo json_encode([ + 'woven' => [ + Sensor::class => is_a(Sensor::class, Proxy::class, true), + SensorValue::class => is_a(SensorValue::class, Proxy::class, true), + ], + 'tables' => [$sensorMetadata->getTableName(), $valueMetadata->getTableName()], + 'targets' => [ + 'Sensor.values' => $sensorMetadata->getAssociationTargetClass('values'), + 'SensorValue.sensor' => $valueMetadata->getAssociationTargetClass('sensor'), + ], + 'mappedSuperclass' => [$sensorMetadata->isMappedSuperclass, $valueMetadata->isMappedSuperclass], + 'persistOutput' => $persistOutput, + 'joinedValues' => $joinedValues, + 'joinOutput' => $joinOutput, + 'lazySensor' => [ + 'class' => $lazySensorClass, + 'uninitialized' => $lazySensorUninitialized, + 'name' => $lazySensorName, + 'initialized' => $lazySensorInitialized, + ], + 'lazyOutput' => $lazyOutput, + 'collectionCount' => $collectionCount, + 'collectionOutput' => $collectionOutput, + 'collectionLoadQueries' => $collectionLoadQueries, + 'queries' => $logger->queries, +], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); diff --git a/tests/Fixtures/project/src/Aspect/EntityFieldAspect.php b/tests/Fixtures/project/src/Aspect/EntityFieldAspect.php index ee19d33c..d2935deb 100644 --- a/tests/Fixtures/project/src/Aspect/EntityFieldAspect.php +++ b/tests/Fixtures/project/src/Aspect/EntityFieldAspect.php @@ -24,4 +24,13 @@ public function beforePersistCallback(MethodInvocation $invocation): void { echo 'persist;'; } + + /** + * Every public method of the two associated entities, as in the pointcut reported in issue #327 + */ + #[Pointcut\Before("execution(public Go\Tests\TestProject\Entity\Sensor->*(*)) || execution(public Go\Tests\TestProject\Entity\SensorValue->*(*))")] + public function beforeSensorMethod(MethodInvocation $invocation): void + { + echo $invocation->getMethod()->getName() . ';'; + } } diff --git a/tests/Fixtures/project/src/Entity/Sensor.php b/tests/Fixtures/project/src/Entity/Sensor.php new file mode 100644 index 00000000..873a586c --- /dev/null +++ b/tests/Fixtures/project/src/Entity/Sensor.php @@ -0,0 +1,58 @@ + */ + #[ORM\OneToMany(targetEntity: SensorValue::class, mappedBy: 'sensor', cascade: ['persist'])] + private Collection $values; + + public function __construct( + #[ORM\Column(type: 'string', length: 64)] + private string $name, + ) { + $this->values = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + /** + * @return Collection + */ + public function getValues(): Collection + { + return $this->values; + } + + public function addValue(int $value): SensorValue + { + $sensorValue = new SensorValue($this, $value); + $this->values->add($sensorValue); + + return $sensorValue; + } +} diff --git a/tests/Fixtures/project/src/Entity/SensorValue.php b/tests/Fixtures/project/src/Entity/SensorValue.php new file mode 100644 index 00000000..975186b4 --- /dev/null +++ b/tests/Fixtures/project/src/Entity/SensorValue.php @@ -0,0 +1,42 @@ +id; + } + + public function getSensor(): Sensor + { + return $this->sensor; + } + + public function getValue(): int + { + return $this->value; + } +} diff --git a/tests/Functional/DoctrineBridgeTest.php b/tests/Functional/DoctrineBridgeTest.php index 11c49ba5..69784682 100644 --- a/tests/Functional/DoctrineBridgeTest.php +++ b/tests/Functional/DoctrineBridgeTest.php @@ -12,13 +12,15 @@ namespace Go\Functional; +use Go\Tests\TestProject\Entity\Sensor; +use Go\Tests\TestProject\Entity\SensorValue; use Go\Tests\TestProject\Entity\WovenEntity; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; /** * Doctrine ORM sees a woven entity exactly once, with every mapping, when entities are discovered - * through WovenEntityClassLocator (issue #671) + * through WovenEntityClassLocator (issue #671), and maps associations between woven entities (issue #327) */ class DoctrineBridgeTest extends BaseFunctionalTestCase { @@ -31,29 +33,21 @@ public function testDoctrineMapsWovenEntity(): void 'Go\\Tests\\TestProject\\Aspect\\EntityFieldAspect->beforeNameAccess', ); - $phpExecutable = (new PhpExecutableFinder())->find(); - $this->assertIsString($phpExecutable); - $process = new Process( - [$phpExecutable, __DIR__ . '/../Fixtures/project/bin/doctrine-metadata.php'], - null, - ['GO_AOP_CONFIGURATION' => $this->getConfigurationName()], - ); - $process->run(); - $this->assertTrue($process->isSuccessful(), $process->getOutput() . $process->getErrorOutput()); - - $result = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR); - $this->assertIsArray($result); + $result = $this->runFixtureScript('doctrine-metadata.php'); - // Discovered through the autoloader, so the woven class is mapped (not the original source) - $this->assertSame([WovenEntity::class], $result['entities']); + // Discovered through the autoloader, so the woven classes are mapped (not the original sources) + $this->assertEqualsCanonicalizing([Sensor::class, SensorValue::class, WovenEntity::class], $result['entities']); $this->assertTrue($result['woven']); $this->assertEqualsCanonicalizing(['id', 'name', 'counter'], $result['fields']); $this->assertSame(['prePersist' => ['beforePersist']], $result['callbacks']); $this->assertSame('woven_entity', $result['table']); $this->assertIsArray($result['schema']); - $this->assertCount(1, $result['schema']); - $createTable = $result['schema'][0]; - $this->assertIsString($createTable); + $createTables = array_values(array_filter( + $result['schema'], + static fn(mixed $sql): bool => is_string($sql) && str_starts_with($sql, 'CREATE TABLE woven_entity '), + )); + $this->assertCount(1, $createTables); + $createTable = $createTables[0]; $this->assertStringContainsString('name VARCHAR(64) NOT NULL', $createTable); $this->assertStringContainsString('counter INTEGER NOT NULL', $createTable); @@ -64,6 +58,94 @@ public function testDoctrineMapsWovenEntity(): void $this->assertSame(3, $result['counter']); } + /** + * Under the 3.x extend-based engine the mapping attributes ended up on the renamed parent class + * (`Entity__AopProxied`): the table name got the suffix, every association pointed at the parent, + * joins were built with mismatched aliases and lazy loading was broken (issue #327). The trait-based + * engine keeps the mapping on the woven class itself, so a bidirectional association between two + * woven entities works as it does without weaving. + */ + public function testDoctrineMapsAssociationsBetweenWovenEntities(): void + { + if (!extension_loaded('pdo_sqlite')) { + $this->markTestSkipped('The pdo_sqlite extension is required to run queries against SQLite'); + } + $this->assertClassIsWoven(Sensor::class); + $this->assertClassIsWoven(SensorValue::class); + + $result = $this->runFixtureScript('doctrine-associations.php'); + + $this->assertSame([Sensor::class => true, SensorValue::class => true], $result['woven']); + $this->assertSame(['sensor', 'sensor_value'], $result['tables']); + $this->assertSame( + ['Sensor.values' => SensorValue::class, 'SensorValue.sensor' => Sensor::class], + $result['targets'], + ); + $this->assertSame([false, false], $result['mappedSuperclass']); + + // Entity methods called by user code are advised, including the constructors + $this->assertSame('__construct;addValue;__construct;addValue;__construct;getId;getValues;getId;getId;', $result['persistOutput']); + + // The DQL join hydrates both sides without running advices; reading the result does + $this->assertSame([20, 25], $result['joinedValues']); + $this->assertSame('getValues;getValue;getValue;', $result['joinOutput']); + + // The many-to-one side is a native lazy object of the woven class, loaded on first access + $this->assertSame( + ['class' => Sensor::class, 'uninitialized' => true, 'name' => 'thermometer', 'initialized' => true], + $result['lazySensor'], + ); + $this->assertSame('getSensor;getName;', $result['lazyOutput']); + + // The collection is loaded lazily with one query over the join column + $this->assertSame(2, $result['collectionCount']); + $this->assertSame('getValues;', $result['collectionOutput']); + $this->assertIsArray($result['collectionLoadQueries']); + $this->assertCount(2, $result['collectionLoadQueries']); + $this->assertSame( + 'SELECT t0.id AS id_1, t0.value AS value_2, t0.sensor_id AS sensor_id_3 FROM sensor_value t0 WHERE t0.sensor_id = ?', + $result['collectionLoadQueries'][1], + ); + + // Every SQL statement references the tables of the woven classes with consistent aliases + $this->assertIsArray($result['queries']); + $joinQuery = array_values(array_filter($result['queries'], static fn(mixed $sql): bool => is_string($sql) && str_contains($sql, 'INNER JOIN'))); + $this->assertCount(1, $joinQuery); + $this->assertSame( + 'SELECT s0_.id AS id_0, s0_.name AS name_1, s1_.id AS id_2, s1_.value AS value_3, s1_.sensor_id AS sensor_id_4' + . ' FROM sensor s0_ INNER JOIN sensor_value s1_ ON s0_.id = s1_.sensor_id WHERE s0_.id = ? ORDER BY s1_.value ASC', + $joinQuery[0], + ); + foreach ($result['queries'] as $sql) { + $this->assertIsString($sql); + $this->assertStringNotContainsString('aop', strtolower($sql)); + } + } + + /** + * Runs a script of the fixture project in a subprocess and decodes its JSON output + * + * @return array + */ + private function runFixtureScript(string $scriptName): array + { + $phpExecutable = (new PhpExecutableFinder())->find(); + $this->assertIsString($phpExecutable); + $process = new Process( + [$phpExecutable, __DIR__ . '/../Fixtures/project/bin/' . $scriptName], + null, + ['GO_AOP_CONFIGURATION' => $this->getConfigurationName()], + ); + $process->run(); + $this->assertTrue($process->isSuccessful(), $process->getOutput() . $process->getErrorOutput()); + + $result = json_decode($process->getOutput(), true, flags: JSON_THROW_ON_ERROR); + $this->assertIsArray($result); + + /** @var array $result */ + return $result; + } + protected function getConfigurationName(): string { return 'default'; From 4b59a5eadae5f68c59c8baf6e1b5e9167eee1b80 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 00:00:26 +0000 Subject: [PATCH 2/3] test(bridge): cover a column mapped in a trait of a woven entity The 3.x metadata listener special-cased mappings imported from traits (Gedmo Timestampable, Blameable) because they were duplicated between the renamed parent and the proxy. Under the trait-based engine the column is mapped once on the woven class: Sensor now uses TimestampableTrait, and the association test asserts the column, its hydration through the lazy object and the advised accessor. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018dKKrvU4XVsKSfKRXPpwUS --- .../project/bin/doctrine-associations.php | 8 ++++--- tests/Fixtures/project/src/Entity/Sensor.php | 8 +++++-- .../project/src/Entity/TimestampableTrait.php | 21 +++++++++++++++++++ tests/Functional/DoctrineBridgeTest.php | 19 ++++++++++++----- 4 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 tests/Fixtures/project/src/Entity/TimestampableTrait.php diff --git a/tests/Fixtures/project/bin/doctrine-associations.php b/tests/Fixtures/project/bin/doctrine-associations.php index 6e1e1790..34f81eb5 100644 --- a/tests/Fixtures/project/bin/doctrine-associations.php +++ b/tests/Fixtures/project/bin/doctrine-associations.php @@ -3,9 +3,9 @@ /** * Boots the fixture project with the AOP kernel and runs two associated woven entities (bidirectional - * one-to-many / many-to-one) through Doctrine ORM against an in-memory SQLite database: schema, persist, - * a DQL join, lazy loading of the many-to-one side and of the collection. Prints what Doctrine sees and - * the SQL it executed as JSON (issue #327). + * one-to-many / many-to-one, one column mapped in a trait) through Doctrine ORM against an in-memory SQLite + * database: schema, persist, a DQL join, lazy loading of the many-to-one side and of the collection. Prints + * what Doctrine sees and the SQL it executed as JSON (issue #327). */ use Doctrine\DBAL\DriverManager; @@ -81,6 +81,7 @@ public function log($level, string|\Stringable $message, array $context = []): v $lazySensorClass = $lazySensor::class; $lazySensorUninitialized = $entityManager->isUninitializedObject($lazySensor); $lazySensorName = $lazySensor->getName(); +$lazySensorCreatedAt = $lazySensor->getCreatedAt()->format('Y-m-d H:i:s'); $lazySensorInitialized = !$entityManager->isUninitializedObject($lazySensor); $entityManager->clear(); $lazyOutput = ob_get_clean(); @@ -112,6 +113,7 @@ public function log($level, string|\Stringable $message, array $context = []): v 'class' => $lazySensorClass, 'uninitialized' => $lazySensorUninitialized, 'name' => $lazySensorName, + 'createdAt' => $lazySensorCreatedAt, 'initialized' => $lazySensorInitialized, ], 'lazyOutput' => $lazyOutput, diff --git a/tests/Fixtures/project/src/Entity/Sensor.php b/tests/Fixtures/project/src/Entity/Sensor.php index 873a586c..b25d463b 100644 --- a/tests/Fixtures/project/src/Entity/Sensor.php +++ b/tests/Fixtures/project/src/Entity/Sensor.php @@ -3,17 +3,20 @@ namespace Go\Tests\TestProject\Entity; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** - * Inverse side of a bidirectional one-to-many between two woven entities (issue #327) + * Inverse side of a bidirectional one-to-many between two woven entities, with a column mapped in a trait (issue #327) */ #[ORM\Entity] #[ORM\Table(name: 'sensor')] class Sensor { + use TimestampableTrait; + #[ORM\Id] #[ORM\Column(type: 'integer')] #[ORM\GeneratedValue] @@ -27,7 +30,8 @@ public function __construct( #[ORM\Column(type: 'string', length: 64)] private string $name, ) { - $this->values = new ArrayCollection(); + $this->values = new ArrayCollection(); + $this->createdAt = new DateTimeImmutable('2017-07-07 21:33:35'); } public function getId(): ?int diff --git a/tests/Fixtures/project/src/Entity/TimestampableTrait.php b/tests/Fixtures/project/src/Entity/TimestampableTrait.php new file mode 100644 index 00000000..441c28cc --- /dev/null +++ b/tests/Fixtures/project/src/Entity/TimestampableTrait.php @@ -0,0 +1,21 @@ +createdAt; + } +} diff --git a/tests/Functional/DoctrineBridgeTest.php b/tests/Functional/DoctrineBridgeTest.php index 69784682..8193e8cd 100644 --- a/tests/Functional/DoctrineBridgeTest.php +++ b/tests/Functional/DoctrineBridgeTest.php @@ -63,7 +63,8 @@ public function testDoctrineMapsWovenEntity(): void * (`Entity__AopProxied`): the table name got the suffix, every association pointed at the parent, * joins were built with mismatched aliases and lazy loading was broken (issue #327). The trait-based * engine keeps the mapping on the woven class itself, so a bidirectional association between two - * woven entities works as it does without weaving. + * woven entities works as it does without weaving. Columns mapped in a trait the entity uses (the + * Gedmo Timestampable case the 3.x metadata listener special-cased) are mapped on the woven class too. */ public function testDoctrineMapsAssociationsBetweenWovenEntities(): void { @@ -90,12 +91,19 @@ public function testDoctrineMapsAssociationsBetweenWovenEntities(): void $this->assertSame([20, 25], $result['joinedValues']); $this->assertSame('getValues;getValue;getValue;', $result['joinOutput']); - // The many-to-one side is a native lazy object of the woven class, loaded on first access + // The many-to-one side is a native lazy object of the woven class, loaded on first access; + // the column mapped in TimestampableTrait is hydrated and its accessor advised like the others $this->assertSame( - ['class' => Sensor::class, 'uninitialized' => true, 'name' => 'thermometer', 'initialized' => true], + [ + 'class' => Sensor::class, + 'uninitialized' => true, + 'name' => 'thermometer', + 'createdAt' => '2017-07-07 21:33:35', + 'initialized' => true, + ], $result['lazySensor'], ); - $this->assertSame('getSensor;getName;', $result['lazyOutput']); + $this->assertSame('getSensor;getName;getCreatedAt;', $result['lazyOutput']); // The collection is loaded lazily with one query over the join column $this->assertSame(2, $result['collectionCount']); @@ -112,7 +120,8 @@ public function testDoctrineMapsAssociationsBetweenWovenEntities(): void $joinQuery = array_values(array_filter($result['queries'], static fn(mixed $sql): bool => is_string($sql) && str_contains($sql, 'INNER JOIN'))); $this->assertCount(1, $joinQuery); $this->assertSame( - 'SELECT s0_.id AS id_0, s0_.name AS name_1, s1_.id AS id_2, s1_.value AS value_3, s1_.sensor_id AS sensor_id_4' + 'SELECT s0_.id AS id_0, s0_.name AS name_1, s0_.createdAt AS createdAt_2,' + . ' s1_.id AS id_3, s1_.value AS value_4, s1_.sensor_id AS sensor_id_5' . ' FROM sensor s0_ INNER JOIN sensor_value s1_ ON s0_.id = s1_.sensor_id WHERE s0_.id = ? ORDER BY s1_.value ASC', $joinQuery[0], ); From 153663e8031318fc2c0c159772959b50bb019891 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 22 Oct 2026 00:08:51 +0000 Subject: [PATCH 3/3] docs(core): keep the Doctrine bridge coverage note short The details live in the test docblock and the fixture script headers. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018dKKrvU4XVsKSfKRXPpwUS --- src/Core/AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/AGENTS.md b/src/Core/AGENTS.md index ef89860d..969c4a64 100644 --- a/src/Core/AGENTS.md +++ b/src/Core/AGENTS.md @@ -24,4 +24,4 @@ src/Bridge/Doctrine (ORM 3.6+, persistence 4.1+ ClassLocator; no metadata surgery needed under the trait engine): - WovenEntityClassLocator — ClassLocator for mapping drivers: parses class names (parser-reflection ReflectionFile, never includes files) and loads them via class_exists() so the autoloader serves the woven class. Doctrine's FileClassLocator require_once's sources: bypasses weaving / misses already-loaded woven entities. - MetadataLoadInterceptor — loadClassMetadata guard: woven entity (implements Go\Aop\Proxy) with a hooked (intercepted) mapped property while native lazy objects are off → InvalidConfigurationException pointing to enableNativeLazyObjects(true). -- Functional coverage: tests/Functional/DoctrineBridgeTest.php — fixture scripts tests/Fixtures/project/bin/doctrine-metadata.php (metadata of one woven entity, no DB connection) and doctrine-associations.php (bidirectional association between two woven entities against in-memory SQLite: DQL join, lazy many-to-one and collection; issue #327, skipped without pdo_sqlite). +- Functional coverage: tests/Functional/DoctrineBridgeTest.php (fixture scripts tests/Fixtures/project/bin/doctrine-*.php; the association one needs pdo_sqlite).