diff --git a/packages/database/module.php b/packages/database/module.php index 55671f32..d76ad934 100644 --- a/packages/database/module.php +++ b/packages/database/module.php @@ -5,11 +5,28 @@ use Marko\Core\Container\ContainerInterface; use Marko\Core\Path\ProjectPaths; use Marko\Database\Connection\TransactionInterface; +use Marko\Database\Entity\EntityDiscovery; +use Marko\Database\Entity\EntityMetadataFactory; use Marko\Database\Seed\SeederDiscovery; use Marko\Database\Seed\SeederDiscoveryInterface; use Marko\Database\Seed\SeederRunner; return [ + 'singletons' => [ + EntityMetadataFactory::class, + ], + 'boot' => function ( + EntityDiscovery $discovery, + EntityMetadataFactory $metadataFactory, + ProjectPaths $paths, + ): void { + $entityClasses = array_merge( + $discovery->discoverInVendor($paths->vendor), + $discovery->discoverInModules($paths->modules), + $discovery->discoverInApp($paths->app), + ); + $metadataFactory->linkExtendersFrom($entityClasses); + }, 'bindings' => [ SeederDiscoveryInterface::class => SeederDiscovery::class, SeederRunner::class => function (ContainerInterface $container): SeederRunner { diff --git a/packages/database/src/Entity/EntityMetadataFactory.php b/packages/database/src/Entity/EntityMetadataFactory.php index 5a8cdcb8..33952a77 100644 --- a/packages/database/src/Entity/EntityMetadataFactory.php +++ b/packages/database/src/Entity/EntityMetadataFactory.php @@ -215,6 +215,33 @@ public function linkExtenders( return $linked; } + /** + * Scan a list of entity classes and link any extenders to their parent metadata. + * + * @param array $entityClasses + */ + public function linkExtendersFrom(array $entityClasses): void + { + $extenders = []; + foreach ($entityClasses as $entityClass) { + $reflection = new ReflectionClass($entityClass); + $tableAttrs = $reflection->getAttributes(Table::class); + if (count($tableAttrs) === 0) { + continue; + } + $tableAttr = $tableAttrs[0]->newInstance(); + if ($tableAttr->extends !== null) { + $extenders[$tableAttr->extends][] = $entityClass; + } + } + foreach ($extenders as $parentClass => $extenderClasses) { + if (!class_exists($parentClass, true)) { + throw EntityException::extenderParentClassNotFound($extenderClasses[0], $parentClass); + } + $this->linkExtenders($parentClass, $extenderClasses); + } + } + /** * Clear the metadata cache. */ diff --git a/packages/database/tests/Entity/EntityMetadataFactoryTest.php b/packages/database/tests/Entity/EntityMetadataFactoryTest.php index 1c2aa2ee..1f1a3ff4 100644 --- a/packages/database/tests/Entity/EntityMetadataFactoryTest.php +++ b/packages/database/tests/Entity/EntityMetadataFactoryTest.php @@ -531,6 +531,30 @@ class UntypedPropertyEntity extends Entity ->toThrow(EntityException::class, "Chained extension is not supported. $extender's parent $parent is itself an extender. Extend the root entity directly."); }); +it('linkExtendersFrom scans entity classes and links extenders to their parents', function (): void { + $this->factory->linkExtendersFrom([ + ExtenderParentEntity::class, + BasicExtenderEntity::class, + ]); + + $parentMetadata = $this->factory->parse(ExtenderParentEntity::class); + + expect($parentMetadata->extenders)->toBe([BasicExtenderEntity::class]); +}); + +it('linkExtendersFrom ignores entities without extends', function (): void { + $this->factory->linkExtendersFrom([ExtenderParentEntity::class]); + + $parentMetadata = $this->factory->parse(ExtenderParentEntity::class); + + expect($parentMetadata->extenders)->toBe([]); +}); + +it('linkExtendersFrom throws when an extender references a parent class that cannot be autoloaded', function (): void { + expect(fn () => $this->factory->linkExtendersFrom([ExtenderWithMissingParentEntity::class])) + ->toThrow(EntityException::class, 'does not exist'); +}); + it('clears cached metadata', function (): void { $entity = new #[Table('test')] class () extends Entity {