diff --git a/packages/database/src/Builder/ModelInspector.php b/packages/database/src/Builder/ModelInspector.php index 59f9b6b64..5c7f76093 100644 --- a/packages/database/src/Builder/ModelInspector.php +++ b/packages/database/src/Builder/ModelInspector.php @@ -18,6 +18,7 @@ use Tempest\Reflection\ClassReflector; use Tempest\Reflection\PropertyReflector; use Tempest\Support\Arr\ImmutableArray; +use Tempest\Support\Memoization\HasMemoization; use Tempest\Validation\Exceptions\ValidationFailed; use Tempest\Validation\SkipValidation; use Tempest\Validation\Validator; @@ -29,14 +30,41 @@ final class ModelInspector { - private(set) ?ClassReflector $reflector; + use HasMemoization; - private(set) object|string $instance; + private static array $inspectors = []; + + private(set) ?ClassReflector $reflector = null; + + private(set) object|string|null $instance = null; private Validator $validator { get => get(Validator::class); } + public static function reset(): void + { + self::$inspectors = []; + } + + public static function forModel(object|string $model): self + { + $key = match (true) { + is_string($model) => $model, + $model instanceof HasMany => $model->property->getIterableType()->getName(), + $model instanceof BelongsTo => $model->property->getType()->getName(), + $model instanceof HasOne => $model->property->getType()->getName(), + $model instanceof ClassReflector => $model->getName(), + default => null, + }; + + if ($key === null) { + return new self($model); + } + + return self::$inspectors[$key] ??= new self($model); + } + public function __construct( private(set) object|string $model, ) { @@ -51,12 +79,11 @@ public function __construct( } else { try { $this->reflector = new ClassReflector($model); + $this->instance = $model; } catch (ReflectionException) { $this->reflector = null; } } - - $this->instance = $model; } public function isObjectModel(): bool @@ -66,27 +93,31 @@ public function isObjectModel(): bool public function getTableDefinition(): TableDefinition { - if (! $this->isObjectModel()) { - return new TableDefinition($this->instance); - } + return $this->memoize('getTableDefinition', function () { + if (! $this->isObjectModel()) { + return new TableDefinition($this->model); + } - $specificName = $this->reflector - ->getAttribute(Table::class) - ?->name; + $specificName = $this->reflector + ->getAttribute(Table::class) + ?->name; - $conventionalName = get(DatabaseConfig::class) - ->namingStrategy - ->getName($this->reflector->getName()); + $conventionalName = get(DatabaseConfig::class) + ->namingStrategy + ->getName($this->reflector->getName()); - return new TableDefinition($specificName ?? $conventionalName); + return new TableDefinition($specificName ?? $conventionalName); + }); } public function getFieldDefinition(string $field): FieldDefinition { - return new FieldDefinition( - $this->getTableDefinition(), - $field, - ); + return $this->memoize('getFieldDefinition' . $field, function () use ($field) { + return new FieldDefinition( + $this->getTableDefinition(), + $field, + ); + }); } public function getTableName(): string @@ -96,162 +127,174 @@ public function getTableName(): string public function getPropertyValues(): array { - if (! $this->isObjectModel()) { - return []; - } + return $this->memoize('getPropertyValues', function () { + if (! $this->isObjectModel()) { + return []; + } - if (! is_object($this->instance)) { - return []; - } + if (! is_object($this->instance)) { + return []; + } - $values = []; + $values = []; - foreach ($this->reflector->getProperties() as $property) { - if ($property->isVirtual()) { - continue; - } + foreach ($this->reflector->getProperties() as $property) { + if ($property->isVirtual()) { + continue; + } - if ($property->hasAttribute(Virtual::class)) { - continue; - } + if ($property->hasAttribute(Virtual::class)) { + continue; + } - if (! $property->isInitialized($this->instance)) { - continue; - } + if (! $property->isInitialized($this->instance)) { + continue; + } - if ($this->getHasMany($property->getName()) || $this->getHasOne($property->getName())) { - continue; - } + if ($this->getHasMany($property->getName()) || $this->getHasOne($property->getName())) { + continue; + } - $name = $property->getName(); + $name = $property->getName(); - $values[$name] = $property->getValue($this->instance); - } + $values[$name] = $property->getValue($this->instance); + } - return $values; + return $values; + }); } public function getBelongsTo(string $name): ?BelongsTo { - if (! $this->isObjectModel()) { - return null; - } + return $this->memoize('getBelongsTo' . $name, function () use ($name) { + if (! $this->isObjectModel()) { + return null; + } - $name = str($name)->camel(); + $name = str($name)->camel(); - $singularizedName = $name->singularizeLastWord(); + $singularizedName = $name->singularizeLastWord(); - if (! $singularizedName->equals($name)) { - return $this->getBelongsTo($singularizedName); - } + if (! $singularizedName->equals($name)) { + return $this->getBelongsTo($singularizedName); + } - if (! $this->reflector->hasProperty($name)) { - return null; - } + if (! $this->reflector->hasProperty($name)) { + return null; + } - $property = $this->reflector->getProperty($name); + $property = $this->reflector->getProperty($name); - if ($belongsTo = $property->getAttribute(BelongsTo::class)) { - return $belongsTo; - } + if ($belongsTo = $property->getAttribute(BelongsTo::class)) { + return $belongsTo; + } - if ($property->hasAttribute(Virtual::class)) { - return null; - } + if ($property->hasAttribute(Virtual::class)) { + return null; + } - if (! $property->getType()->isRelation()) { - return null; - } + if (! $property->getType()->isRelation()) { + return null; + } - if ($property->hasAttribute(SerializeWith::class) || $property->getType()->asClass()->hasAttribute(SerializeWith::class)) { - return null; - } + if ($property->hasAttribute(SerializeWith::class) || $property->getType()->asClass()->hasAttribute(SerializeWith::class)) { + return null; + } - if ($property->getType()->asClass()->hasAttribute(SerializeAs::class)) { - return null; - } + if ($property->getType()->asClass()->hasAttribute(SerializeAs::class)) { + return null; + } - if ($property->hasAttribute(HasOne::class)) { - return null; - } + if ($property->hasAttribute(HasOne::class)) { + return null; + } - $belongsTo = new BelongsTo(); - $belongsTo->property = $property; + $belongsTo = new BelongsTo(); + $belongsTo->property = $property; - return $belongsTo; + return $belongsTo; + }); } public function getHasOne(string $name): ?HasOne { - if (! $this->isObjectModel()) { - return null; - } + return $this->memoize('getHasOne' . $name, function () use ($name) { + if (! $this->isObjectModel()) { + return null; + } - $name = str($name)->camel(); + $name = str($name)->camel(); - $singularizedName = $name->singularizeLastWord(); + $singularizedName = $name->singularizeLastWord(); - if (! $singularizedName->equals($name)) { - return $this->getHasOne($singularizedName); - } + if (! $singularizedName->equals($name)) { + return $this->getHasOne($singularizedName); + } - if (! $this->reflector->hasProperty($name)) { - return null; - } + if (! $this->reflector->hasProperty($name)) { + return null; + } - $property = $this->reflector->getProperty($name); + $property = $this->reflector->getProperty($name); - if ($hasOne = $property->getAttribute(HasOne::class)) { - return $hasOne; - } + if ($hasOne = $property->getAttribute(HasOne::class)) { + return $hasOne; + } - return null; + return null; + }); } public function getHasMany(string $name): ?HasMany { - if (! $this->isObjectModel()) { - return null; - } + return $this->memoize('getHasMany' . $name, function () use ($name) { + if (! $this->isObjectModel()) { + return null; + } - $name = str($name)->camel(); + $name = str($name)->camel(); - if (! $this->reflector->hasProperty($name)) { - return null; - } + if (! $this->reflector->hasProperty($name)) { + return null; + } - $property = $this->reflector->getProperty($name); + $property = $this->reflector->getProperty($name); - if ($hasMany = $property->getAttribute(HasMany::class)) { - return $hasMany; - } + if ($hasMany = $property->getAttribute(HasMany::class)) { + return $hasMany; + } - if ($property->hasAttribute(Virtual::class)) { - return null; - } + if ($property->hasAttribute(Virtual::class)) { + return null; + } - if (! $property->getIterableType()?->isRelation()) { - return null; - } + if (! $property->getIterableType()?->isRelation()) { + return null; + } - $hasMany = new HasMany(); - $hasMany->property = $property; + $hasMany = new HasMany(); + $hasMany->property = $property; - return $hasMany; + return $hasMany; + }); } public function isRelation(string|PropertyReflector $name): bool { $name = $name instanceof PropertyReflector ? $name->getName() : $name; - return $this->getBelongsTo($name) !== null || $this->getHasOne($name) !== null || $this->getHasMany($name) !== null; + return $this->memoize('isRelation' . $name, function () use ($name) { + return $this->getBelongsTo($name) !== null || $this->getHasOne($name) !== null || $this->getHasMany($name) !== null; + }); } public function getRelation(string|PropertyReflector $name): ?Relation { $name = $name instanceof PropertyReflector ? $name->getName() : $name; - return $this->getBelongsTo($name) ?? $this->getHasOne($name) ?? $this->getHasMany($name); + return $this->memoize('getRelation' . $name, function () use ($name) { + return $this->getBelongsTo($name) ?? $this->getHasOne($name) ?? $this->getHasMany($name); + }); } /** @@ -259,19 +302,21 @@ public function getRelation(string|PropertyReflector $name): ?Relation */ public function getRelations(): ImmutableArray { - if (! $this->isObjectModel()) { - return arr(); - } + return $this->memoize('getRelations', function () { + if (! $this->isObjectModel()) { + return arr(); + } - $relationFields = arr(); + $relationFields = arr(); - foreach ($this->reflector->getPublicProperties() as $property) { - if ($relation = $this->getRelation($property->getName())) { - $relationFields[] = $relation; + foreach ($this->reflector->getPublicProperties() as $property) { + if ($relation = $this->getRelation($property->getName())) { + $relationFields[] = $relation; + } } - } - return $relationFields; + return $relationFields; + }); } /** @@ -491,7 +536,7 @@ public function getName(): string return $this->reflector->getName(); } - return $this->instance; + return $this->model; } public function getQualifiedPrimaryKey(): ?string diff --git a/packages/database/src/functions.php b/packages/database/src/functions.php index c635c96ef..af93d2bc9 100644 --- a/packages/database/src/functions.php +++ b/packages/database/src/functions.php @@ -26,6 +26,6 @@ function query(string|object $model): QueryBuilder */ function inspect(string|object $model): ModelInspector { - return new ModelInspector($model); + return ModelInspector::forModel($model); } } diff --git a/packages/mapper/src/CasterFactory.php b/packages/mapper/src/CasterFactory.php index 9151ba185..3e5c7dd3e 100644 --- a/packages/mapper/src/CasterFactory.php +++ b/packages/mapper/src/CasterFactory.php @@ -8,11 +8,14 @@ use Tempest\Container\Container; use Tempest\Container\Singleton; use Tempest\Reflection\PropertyReflector; +use Tempest\Support\Memoization\HasMemoization; use UnitEnum; #[Singleton] final class CasterFactory { + use HasMemoization; + /** * @var array, int}[]> */ @@ -44,45 +47,48 @@ public function addCaster(string $casterClass, int $priority = 0, Context|UnitEn */ public function in(Context|UnitEnum|string $context): self { - $serializer = clone $this; - $serializer->context = $context; + $caster = clone $this; + $caster->context = $context; - return $serializer; + return $caster; } public function forProperty(PropertyReflector $property): ?Caster { $context = MappingContext::from($this->context); - $type = $property->getType(); - $castWith = $property->getAttribute(CastWith::class); - - if ($castWith === null && $type->isClass()) { - $castWith = $type->asClass()->getAttribute(CastWith::class, recursive: true); - } - if ($castWith) { - return $this->container->get($castWith->className, context: $context); - } + return $this->memoize('[' . $context->name . '] ' . $property->getName(), function () use ($property, $context) { + $type = $property->getType(); + $castWith = $property->getAttribute(CastWith::class); - if ($casterAttribute = $property->getAttribute(ProvidesCaster::class)) { - return $this->container->get($casterAttribute->caster, context: $context); - } + if ($castWith === null && $type->isClass()) { + $castWith = $type->asClass()->getAttribute(CastWith::class, recursive: true); + } - foreach ($this->resolveCasters() as [$casterClass]) { - if (is_a($casterClass, DynamicCaster::class, allow_string: true)) { - if (! $casterClass::accepts($property)) { - continue; - } + if ($castWith) { + return $this->container->get($castWith->className, context: $context); } - if (is_a($casterClass, ConfigurableCaster::class, allow_string: true)) { - return $casterClass::configure($property, $context); + if ($casterAttribute = $property->getAttribute(ProvidesCaster::class)) { + return $this->container->get($casterAttribute->caster, context: $context); } - return $this->container->get($casterClass, context: $context); - } + foreach ($this->resolveCasters() as [$casterClass]) { + if (is_a($casterClass, DynamicCaster::class, allow_string: true)) { + if (! $casterClass::accepts($property)) { + continue; + } + } + + if (is_a($casterClass, ConfigurableCaster::class, allow_string: true)) { + return $casterClass::configure($property, $context); + } + + return $this->container->get($casterClass, context: $context); + } - return null; + return null; + }); } /** diff --git a/packages/mapper/src/Mappers/ArrayToObjectMapper.php b/packages/mapper/src/Mappers/ArrayToObjectMapper.php index af1bd9395..7694cfee9 100644 --- a/packages/mapper/src/Mappers/ArrayToObjectMapper.php +++ b/packages/mapper/src/Mappers/ArrayToObjectMapper.php @@ -13,15 +13,18 @@ use Tempest\Reflection\ClassReflector; use Tempest\Reflection\PropertyReflector; use Tempest\Support\Arr; +use Tempest\Support\Memoization\HasMemoization; use Throwable; use function Tempest\Support\arr; -final readonly class ArrayToObjectMapper implements Mapper +final class ArrayToObjectMapper implements Mapper { + use HasMemoization; + public function __construct( - private CasterFactory $casterFactory, - private Context $context, + private readonly CasterFactory $casterFactory, + private readonly Context $context, ) {} public function canMap(mixed $from, mixed $to): bool @@ -158,9 +161,11 @@ private function setChildParentRelation(object $parent, mixed $child, ClassRefle public function resolveValue(PropertyReflector $property, mixed $value): mixed { - $caster = $this->casterFactory - ->in($this->context) - ->forProperty($property); + $caster = $this->memoize((string) $property, function () use ($property, $value) { + return $this->casterFactory + ->in($this->context) + ->forProperty($property); + }); if ($property->isNullable() && $value === null) { return null; diff --git a/packages/mapper/src/SerializerFactory.php b/packages/mapper/src/SerializerFactory.php index 645c068f5..d82f19ffa 100644 --- a/packages/mapper/src/SerializerFactory.php +++ b/packages/mapper/src/SerializerFactory.php @@ -10,11 +10,14 @@ use Tempest\Reflection\ClassReflector; use Tempest\Reflection\PropertyReflector; use Tempest\Reflection\TypeReflector; +use Tempest\Support\Memoization\HasMemoization; use UnitEnum; #[Singleton] final class SerializerFactory { + use HasMemoization; + /** * @var array,int}[]> */ @@ -55,36 +58,40 @@ public function in(Context|UnitEnum|string $context): self public function forProperty(PropertyReflector $property): ?Serializer { $context = MappingContext::from($this->context); - $type = $property->getType(); - $serializeWith = $property->getAttribute(SerializeWith::class); - if ($serializeWith === null && $type->isClass()) { - $serializeWith = $type->asClass()->getAttribute(SerializeWith::class, recursive: true); - } + return $this->memoize('[' . $context->name . '] ' . $property->getName(), function () use ($property, $context) { + $context = MappingContext::from($this->context); + $type = $property->getType(); + $serializeWith = $property->getAttribute(SerializeWith::class); - if ($serializeWith !== null) { - return $this->container->get($serializeWith->className, context: $context); - } + if ($serializeWith === null && $type->isClass()) { + $serializeWith = $type->asClass()->getAttribute(SerializeWith::class, recursive: true); + } - if ($serializerAttribute = $property->getAttribute(ProvidesSerializer::class)) { - return $this->container->get($serializerAttribute->serializer, context: $context); - } + if ($serializeWith !== null) { + return $this->container->get($serializeWith->className, context: $context); + } - foreach ($this->resolveSerializers() as [$serializerClass]) { - if (is_a($serializerClass, DynamicSerializer::class, allow_string: true)) { - if (! $serializerClass::accepts($property)) { - continue; - } + if ($serializerAttribute = $property->getAttribute(ProvidesSerializer::class)) { + return $this->container->get($serializerAttribute->serializer, context: $context); } - $serializer = $this->resolveSerializer($serializerClass, $property); + foreach ($this->resolveSerializers() as [$serializerClass]) { + if (is_a($serializerClass, DynamicSerializer::class, allow_string: true)) { + if (! $serializerClass::accepts($property)) { + continue; + } + } - if ($serializer !== null) { - return $serializer; + $serializer = $this->resolveSerializer($serializerClass, $property); + + if ($serializer !== null) { + return $serializer; + } } - } - return null; + return null; + }); } public function forValue(mixed $value): ?Serializer diff --git a/packages/reflection/src/PropertyReflector.php b/packages/reflection/src/PropertyReflector.php index 455d8c898..3eda1ce13 100644 --- a/packages/reflection/src/PropertyReflector.php +++ b/packages/reflection/src/PropertyReflector.php @@ -157,4 +157,9 @@ public function hasDefaultValue(): bool return $hasDefaultValue || $hasPromotedDefaultValue; } + + public function __toString(): string + { + return $this->getClass()->getName() . '::' . $this->getName(); + } } diff --git a/packages/support/src/Memoization/HasMemoization.php b/packages/support/src/Memoization/HasMemoization.php new file mode 100644 index 000000000..cca26ef69 --- /dev/null +++ b/packages/support/src/Memoization/HasMemoization.php @@ -0,0 +1,19 @@ +memoize)) { + $this->memoize[$key] = $closure(); + } + + return $this->memoize[$key]; + } +} diff --git a/tests/Integration/FrameworkIntegrationTestCase.php b/tests/Integration/FrameworkIntegrationTestCase.php index 61ba1657f..4dda31142 100644 --- a/tests/Integration/FrameworkIntegrationTestCase.php +++ b/tests/Integration/FrameworkIntegrationTestCase.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use Stringable; +use Tempest\Database\Builder\ModelInspector; use Tempest\Database\DatabaseInitializer; use Tempest\Discovery\DiscoveryLocation; use Tempest\Framework\Testing\IntegrationTest; @@ -54,6 +55,8 @@ protected function setUp(): void $this->container->config(require $databaseConfigPath); $this->database->reset(migrate: false); + + ModelInspector::reset(); } protected function render(string|View $view, mixed ...$params): string