Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 168 additions & 123 deletions packages/database/src/Builder/ModelInspector.php

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/database/src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ function query(string|object $model): QueryBuilder
*/
function inspect(string|object $model): ModelInspector
{
return new ModelInspector($model);
return ModelInspector::forModel($model);
}
}
56 changes: 31 additions & 25 deletions packages/mapper/src/CasterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, array{class-string<\Tempest\Mapper\Caster>, int}[]>
*/
Expand Down Expand Up @@ -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;
});
}

/**
Expand Down
17 changes: 11 additions & 6 deletions packages/mapper/src/Mappers/ArrayToObjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
49 changes: 28 additions & 21 deletions packages/mapper/src/SerializerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,array{class-string<\Tempest\Mapper\Serializer>,int}[]>
*/
Expand Down Expand Up @@ -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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$context = MappingContext::from($this->context);

Why do we need to create $context here again? Can't we just go with the one that we already imported via the use?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, @innocenzi I think you wrote that part? I simply moved it around with this PR.

$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
Expand Down
5 changes: 5 additions & 0 deletions packages/reflection/src/PropertyReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,9 @@ public function hasDefaultValue(): bool

return $hasDefaultValue || $hasPromotedDefaultValue;
}

public function __toString(): string
{
return $this->getClass()->getName() . '::' . $this->getName();
}
}
19 changes: 19 additions & 0 deletions packages/support/src/Memoization/HasMemoization.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Tempest\Support\Memoization;

use Closure;

trait HasMemoization
{
private array $memoize = [];

private function memoize(string $key, Closure $closure): mixed
{
if (! array_key_exists($key, $this->memoize)) {
$this->memoize[$key] = $closure();
}

return $this->memoize[$key];
}
}
3 changes: 3 additions & 0 deletions tests/Integration/FrameworkIntegrationTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down