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
10 changes: 0 additions & 10 deletions packages/database/src/Casters/IdCaster.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

use Tempest\Database\Id;
use Tempest\Mapper\Caster;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;

final readonly class IdCaster implements Caster
{
Expand All @@ -18,13 +17,4 @@ public function cast(mixed $input): Id

return new Id($input);
}

public function serialize(mixed $input): string
{
if (! ($input instanceof Id)) {
throw new ValueCouldNotBeSerialized(Id::class);
}

return (string) $input->id;
}
}
2 changes: 2 additions & 0 deletions packages/database/src/DatabaseInitializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Tempest\Database\Connection\Connection;
use Tempest\Database\Connection\PDOConnection;
use Tempest\Database\Transactions\GenericTransactionManager;
use Tempest\Mapper\SerializerFactory;
use Tempest\Reflection\ClassReflector;
use UnitEnum;

Expand Down Expand Up @@ -42,6 +43,7 @@ className: Connection::class,
return new GenericDatabase(
$connection,
new GenericTransactionManager($connection),
$container->get(SerializerFactory::class),
);
}
}
19 changes: 4 additions & 15 deletions packages/database/src/GenericDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@

namespace Tempest\Database;

use BackedEnum;
use DateTimeInterface;
use PDO;
use PDOException;
use PDOStatement;
Expand All @@ -14,6 +12,7 @@
use Tempest\Database\Connection\Connection;
use Tempest\Database\Exceptions\QueryWasInvalid;
use Tempest\Database\Transactions\TransactionManager;
use Tempest\Mapper\SerializerFactory;
use Throwable;
use UnitEnum;

Expand All @@ -33,6 +32,7 @@ final class GenericDatabase implements Database
public function __construct(
private(set) readonly Connection $connection,
private(set) readonly TransactionManager $transactionManager,
private(set) readonly SerializerFactory $serializerFactory,
) {}

public function execute(BuildsQuery|Query $query): void
Expand Down Expand Up @@ -124,21 +124,10 @@ private function resolveBindings(Query $query): array
$bindings = [];

foreach ($query->bindings as $key => $value) {
// TODO: this should be handled by serializers (except the Query)
if ($value instanceof Id) {
$value = $value->id;
}

if ($value instanceof Query) {
$value = $value->execute();
}

if ($value instanceof BackedEnum) {
$value = $value->value;
}

if ($value instanceof DateTimeInterface) {
$value = $value->format('Y-m-d H:i:s');
} elseif ($serializer = $this->serializerFactory->forValue($value)) {
$value = $serializer->serialize($value);
}

$bindings[$key] = $value;
Expand Down
19 changes: 19 additions & 0 deletions packages/database/src/Serializers/IdSerializer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Tempest\Database\Serializers;

use Tempest\Database\Id;
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Serializer;

final class IdSerializer implements Serializer
{
public function serialize(mixed $input): array|string
{
if (! ($input instanceof Id)) {
throw new ValueCouldNotBeSerialized(Id::class);
}

return $input->id;
}
}
21 changes: 21 additions & 0 deletions packages/database/src/Serializers/IdSerializerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Tempest\Database\Serializers;

use Tempest\Core\KernelEvent;
use Tempest\Database\Id;
use Tempest\EventBus\EventHandler;
use Tempest\Mapper\SerializerFactory;

final readonly class IdSerializerProvider
{
public function __construct(
private SerializerFactory $serializerFactory,
) {}

#[EventHandler(KernelEvent::BOOTED)]
public function __invoke(KernelEvent $_event): void
{
$this->serializerFactory->addSerializer(Id::class, IdSerializer::class);
}
}
5 changes: 3 additions & 2 deletions packages/database/tests/GenericDatabaseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Tempest\Database\Connection\Connection;
use Tempest\Database\GenericDatabase;
use Tempest\Database\Transactions\GenericTransactionManager;
use Tempest\Mapper\SerializerFactory;

/**
* @internal
Expand All @@ -33,7 +34,7 @@ public function test_it_executes_transactions(): void
$database = new GenericDatabase(
$connection,
new GenericTransactionManager($connection),
DatabaseDialect::SQLITE,
new SerializerFactory(),
);

$result = $database->withinTransaction(function () {
Expand All @@ -60,7 +61,7 @@ public function test_it_rolls_back_transactions_on_failure(): void
$database = new GenericDatabase(
$connection,
new GenericTransactionManager($connection),
DatabaseDialect::SQLITE,
new SerializerFactory(),
);

$result = $database->withinTransaction(function (): never {
Expand Down
30 changes: 23 additions & 7 deletions packages/mapper/src/Mappers/ObjectToArrayMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use Tempest\Reflection\ClassReflector;
use Tempest\Reflection\PropertyReflector;

use function Tempest\map;

final readonly class ObjectToArrayMapper implements Mapper
{
public function __construct(
Expand All @@ -22,20 +24,24 @@ public function canMap(mixed $from, mixed $to): bool
return false;
}

public function map(mixed $from, mixed $to): array
public function map(mixed $from, mixed $to): mixed
{
if ($from instanceof JsonSerializable) {
return $from->jsonSerialize();
}

$class = new ClassReflector($from);
if (is_object($from)) {
$class = new ClassReflector($from);

$mappedProperties = [];
$mappedProperties = [];

foreach ($class->getPublicProperties() as $property) {
$propertyName = $this->resolvePropertyName($property);
$propertyValue = $this->resolvePropertyValue($property, $from);
$mappedProperties[$propertyName] = $propertyValue;
foreach ($class->getPublicProperties() as $property) {
$propertyName = $this->resolvePropertyName($property);
$propertyValue = $this->resolvePropertyValue($property, $from);
$mappedProperties[$propertyName] = $propertyValue;
}
} else {
$mappedProperties = $from;
}

return $mappedProperties;
Expand All @@ -45,6 +51,16 @@ private function resolvePropertyValue(PropertyReflector $property, object $objec
{
$propertyValue = $property->getValue($object);

if ($property->getIterableType()?->isClass()) {
foreach ($propertyValue as $key => $value) {
if (is_object($value)) {
$propertyValue[$key] = map($value)->toArray();
}
}

return $propertyValue;
}

if ($propertyValue !== null && ($serializer = $this->serializerFactory->forProperty($property)) !== null) {
return $serializer->serialize($propertyValue);
}
Expand Down
77 changes: 73 additions & 4 deletions packages/mapper/src/SerializerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
namespace Tempest\Mapper;

use Closure;
use Tempest\Reflection\ClassReflector;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;
use TypeError;

use function Tempest\get;

Expand All @@ -26,6 +29,40 @@ public function addSerializer(string|Closure $for, string|Closure $serializerCla
return $this;
}

private function serializerMatches(Closure|string $for, TypeReflector|string $input): bool
{
if (is_callable($for)) {
try {
return $for($input);
} catch (TypeError) { // @mago-expect best-practices/dont-catch-error
return false;
}
}

if ($for === $input) {
return true;
}

if ($input instanceof TypeReflector) {
return $input->getName() === $for || $input->matches($for);
}

return false;
}

private function resolveSerializer(Closure|string $serializerClass, PropertyReflector|TypeReflector|string $input): ?Serializer
{
if (is_string($serializerClass)) {
return get($serializerClass);
}

try {
return $serializerClass($input);
} catch (TypeError) { // @mago-expect best-practices/dont-catch-error
return null;
}
}

public function forProperty(PropertyReflector $property): ?Serializer
{
$type = $property->getType();
Expand All @@ -46,10 +83,42 @@ public function forProperty(PropertyReflector $property): ?Serializer

// Resolve serializer from manual additions
foreach ($this->serializers as [$for, $serializerClass]) {
if (is_callable($for) && $for($property) || is_string($for) && $type->matches($for) || $type->getName() === $for) {
return is_callable($serializerClass)
? $serializerClass($property)
: get($serializerClass);
if (! $this->serializerMatches($for, $type)) {
continue;
}

$serializer = $this->resolveSerializer($serializerClass, $property);

if ($serializer !== null) {
return $serializer;
}
}

return null;
}

public function forValue(mixed $value): ?Serializer
{
if ($value === null) {
return null;
}

if (is_object($value)) {
$input = new ClassReflector($value)->getType();
} else {
$input = gettype($value);
}

// Resolve serializer from manual additions
foreach ($this->serializers as [$for, $serializerClass]) {
if (! $this->serializerMatches($for, $input)) {
continue;
}

$serializer = $this->resolveSerializer($serializerClass, $input);

if ($serializer !== null) {
return $serializer;
}
}

Expand Down
22 changes: 15 additions & 7 deletions packages/mapper/src/SerializerFactoryInitializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Tempest\Container\Container;
use Tempest\Container\Initializer;
use Tempest\Container\Singleton;
use Tempest\Database\Id;
use Tempest\DateTime\DateTime;
use Tempest\DateTime\DateTimeInterface;
use Tempest\Mapper\Serializers\ArrayOfObjectsSerializer;
Expand All @@ -27,6 +28,7 @@
use Tempest\Mapper\Serializers\SerializableSerializer;
use Tempest\Mapper\Serializers\StringSerializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;

final class SerializerFactoryInitializer implements Initializer
{
Expand All @@ -35,19 +37,25 @@ public function initialize(Container $container): SerializerFactory
{
return new SerializerFactory()
->addSerializer('bool', BooleanSerializer::class)
->addSerializer('boolean', BooleanSerializer::class)
->addSerializer('float', FloatSerializer::class)
->addSerializer('double', FloatSerializer::class)
->addSerializer('int', IntegerSerializer::class)
->addSerializer('integer', IntegerSerializer::class)
->addSerializer('string', StringSerializer::class)
->addSerializer('array', ArrayToJsonSerializer::class)
->addSerializer(DateTimeInterface::class, DateTimeSerializer::fromProperty(...))
->addSerializer(NativeDateTimeImmutable::class, NativeDateTimeSerializer::fromProperty(...))
->addSerializer(NativeDateTimeInterface::class, NativeDateTimeSerializer::fromProperty(...))
->addSerializer(NativeDateTime::class, NativeDateTimeSerializer::fromProperty(...))
->addSerializer(Stringable::class, StringSerializer::class)
->addSerializer(DateTimeInterface::class, DateTimeSerializer::fromReflector(...))
->addSerializer(NativeDateTimeImmutable::class, NativeDateTimeSerializer::fromReflector(...))
->addSerializer(NativeDateTimeInterface::class, NativeDateTimeSerializer::fromReflector(...))
->addSerializer(NativeDateTime::class, NativeDateTimeSerializer::fromReflector(...))
->addSerializer(Serializable::class, SerializableSerializer::class)
->addSerializer(JsonSerializable::class, SerializableSerializer::class)
->addSerializer(Stringable::class, StringSerializer::class)
->addSerializer(BackedEnum::class, EnumSerializer::class)
->addSerializer(DateTime::class, DateTimeSerializer::fromProperty(...))
->addSerializer(fn (PropertyReflector $property) => $property->getIterableType() !== null, ArrayOfObjectsSerializer::class);
->addSerializer(DateTime::class, DateTimeSerializer::fromReflector(...))
->addSerializer(
fn (PropertyReflector $property) => $property->getIterableType() !== null,
ArrayOfObjectsSerializer::class,
);
}
}
11 changes: 8 additions & 3 deletions packages/mapper/src/Serializers/DateTimeSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,22 @@
use Tempest\Mapper\Exceptions\ValueCouldNotBeSerialized;
use Tempest\Mapper\Serializer;
use Tempest\Reflection\PropertyReflector;
use Tempest\Reflection\TypeReflector;
use Tempest\Validation\Rules\DateTimeFormat;

final readonly class DateTimeSerializer implements Serializer
{
public function __construct(
private FormatPattern|string $format = FormatPattern::ISO8601,
private FormatPattern|string $format = FormatPattern::SQL_DATE_TIME,
) {}

public static function fromProperty(PropertyReflector $property): self
public static function fromReflector(PropertyReflector|TypeReflector $reflector): self
{
$format = $property->getAttribute(DateTimeFormat::class)->format ?? FormatPattern::ISO8601;
if ($reflector instanceof PropertyReflector) {
$format = $reflector->getAttribute(DateTimeFormat::class)?->format ?? FormatPattern::SQL_DATE_TIME;
} else {
$format = FormatPattern::SQL_DATE_TIME;
}

return new self($format);
}
Expand Down
Loading