From adc34ac777406ce4d37b18c4e8bc790a49824d28 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Thu, 12 Oct 2023 20:56:53 +0200 Subject: [PATCH 1/2] feat: support multiple transformers per class --- src/Attributes/WithTransformer.php | 2 +- src/Support/DataConfig.php | 29 +++++++++++++++--------- src/Support/DataProperty.php | 5 ++-- src/Transformers/DataTransformer.php | 34 ++++++++++++++++++---------- tests/Support/DataPropertyTest.php | 4 ++-- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/src/Attributes/WithTransformer.php b/src/Attributes/WithTransformer.php index 62786d7a..177fda98 100644 --- a/src/Attributes/WithTransformer.php +++ b/src/Attributes/WithTransformer.php @@ -6,7 +6,7 @@ use Spatie\LaravelData\Exceptions\CannotCreateTransformerAttribute; use Spatie\LaravelData\Transformers\Transformer; -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] +#[Attribute(Attribute::IS_REPEATABLE | Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] class WithTransformer { public array $arguments; diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index 6c73545b..12bd5f34 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -2,17 +2,17 @@ namespace Spatie\LaravelData\Support; +use Illuminate\Support\Arr; use ReflectionClass; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Transformers\Transformer; class DataConfig { /** @var array */ protected array $dataClasses = []; - /** @var array */ + /** @var array */ protected array $transformers = []; /** @var array */ @@ -33,9 +33,7 @@ public function __construct(array $config) $config['rule_inferrers'] ?? [] ); - foreach ($config['transformers'] ?? [] as $transformable => $transformer) { - $this->transformers[ltrim($transformable, ' \\')] = app($transformer); - } + $this->setTransformers($config['transformers'] ?? []); foreach ($config['casts'] ?? [] as $castable => $cast) { $this->casts[ltrim($castable, ' \\')] = app($cast); @@ -44,6 +42,15 @@ public function __construct(array $config) $this->morphMap = new DataClassMorphMap(); } + public function setTransformers(array $transformers): self + { + foreach ($transformers as $transformable => $transformers) { + $this->transformers[ltrim($transformable, ' \\')] = array_map(resolve(...), Arr::wrap($transformers)); + } + + return $this; + } + public function getDataClass(string $class): DataClass { if (array_key_exists($class, $this->dataClasses)) { @@ -75,23 +82,23 @@ public function findGlobalCastForProperty(DataProperty $property): ?Cast return null; } - public function findGlobalTransformerForValue(mixed $value): ?Transformer + public function findGlobalTransformersForValue(mixed $value): array { if (gettype($value) !== 'object') { - return null; + return []; } - foreach ($this->transformers as $transformable => $transformer) { + foreach ($this->transformers as $transformable => $transformers) { if ($value::class === $transformable) { - return $transformer; + return $transformers; } if (is_a($value::class, $transformable, true)) { - return $transformer; + return $transformers; } } - return null; + return []; } public function getRuleInferrers(): array diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index a56f9f31..318b455e 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -13,7 +13,6 @@ use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Mappers\NameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; -use Spatie\LaravelData\Transformers\Transformer; /** * @property Collection $attributes @@ -32,7 +31,7 @@ public function __construct( public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, public readonly ?Cast $cast, - public readonly ?Transformer $transformer, + public readonly Collection $transformers, public readonly ?string $inputMappedName, public readonly ?string $outputMappedName, public readonly Collection $attributes, @@ -86,7 +85,7 @@ className: $property->class, hasDefaultValue: $property->isPromoted() ? $hasDefaultValue : $property->hasDefaultValue(), defaultValue: $property->isPromoted() ? $defaultValue : $property->getDefaultValue(), cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(), - transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer)?->get(), + transformers: $attributes->filter(fn (object $attribute) => $attribute instanceof WithTransformer)->map->get(), inputMappedName: $inputMappedName, outputMappedName: $outputMappedName, attributes: $attributes, diff --git a/src/Transformers/DataTransformer.php b/src/Transformers/DataTransformer.php index 8a54fef2..293ac3f0 100644 --- a/src/Transformers/DataTransformer.php +++ b/src/Transformers/DataTransformer.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Transformers; use Illuminate\Support\Arr; +use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; @@ -215,8 +216,12 @@ protected function resolvePropertyValue( $value = Arr::except($value, $trees->except->getFields()); } - if ($transformer = $this->resolveTransformerForValue($property, $value)) { - return $transformer->transform($property, $value); + if (count($transformers = $this->resolveTransformersForValue($property, $value))) { + foreach ($transformers as $transformer) { + if ($result = $transformer->transform($property, $value)) { + return $result; + } + } } if (! $value instanceof BaseData && ! $value instanceof BaseDataCollectable) { @@ -244,23 +249,28 @@ protected function resolvePropertyValue( return $value; } - protected function resolveTransformerForValue( + protected function resolveTransformersForValue( DataProperty $property, mixed $value, - ): ?Transformer { + ): Collection { if (! $this->transformValues) { - return null; + return collect(); } - $transformer = $property->transformer ?? $this->config->findGlobalTransformerForValue($value); + // NOTE: maybe these should be merged instead, but that would be a breaking change. + $transformers = $property->transformers->isNotEmpty() + ? $property->transformers + : collect($this->config->findGlobalTransformersForValue($value)); - $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer - && ($property->type->isDataObject || $property->type->isDataCollectable); + return $transformers->filter(function (Transformer $transformer) use ($property) { + $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer + && ($property->type->isDataObject || $property->type->isDataCollectable); - if ($shouldUseDefaultDataTransformer) { - return null; - } + if ($shouldUseDefaultDataTransformer) { + return false; + } - return $transformer; + return true; + }); } } diff --git a/tests/Support/DataPropertyTest.php b/tests/Support/DataPropertyTest.php index 6e354a5a..a6d5383e 100644 --- a/tests/Support/DataPropertyTest.php +++ b/tests/Support/DataPropertyTest.php @@ -41,7 +41,7 @@ function resolveHelper( public SimpleData $property; }); - expect($helper->transformer)->toEqual(new DateTimeInterfaceTransformer()); + expect($helper->transformers->first())->toEqual(new DateTimeInterfaceTransformer()); }); it('can get the transformer attribute with arguments', function () { @@ -50,7 +50,7 @@ function resolveHelper( public SimpleData $property; }); - expect($helper->transformer)->toEqual(new DateTimeInterfaceTransformer('d-m-y')); + expect($helper->transformers->first())->toEqual(new DateTimeInterfaceTransformer('d-m-y')); }); it('can get the mapped input name', function () { From 0a7c6816368f02ec0d9acb0598a24713fb76ae87 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Thu, 12 Oct 2023 23:57:50 +0200 Subject: [PATCH 2/2] feat!: add context to transformers --- src/Concerns/BaseData.php | 3 ++- src/Concerns/BaseDataCollectable.php | 2 ++ src/Concerns/ResponsableData.php | 1 + src/Concerns/TransformableData.php | 11 ++++++++--- src/Contracts/TransformableData.php | 3 +++ src/Support/EloquentCasts/DataEloquentCast.php | 4 ++-- src/Transformers/ArrayableTransformer.php | 2 +- src/Transformers/DataCollectableTransformer.php | 6 +++++- src/Transformers/DataTransformer.php | 17 ++++++++++------- .../DateTimeInterfaceTransformer.php | 2 +- src/Transformers/EnumTransformer.php | 2 +- src/Transformers/Transformer.php | 2 +- .../ConfidentialDataCollectionTransformer.php | 4 ++-- .../ConfidentialDataTransformer.php | 2 +- .../Transformers/StringToUpperTransformer.php | 2 +- .../DateTimeInterfaceTransformerTest.php | 4 ---- 16 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 5892096d..3dd3ec56 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -102,8 +102,9 @@ public function transform( bool $transformValues = true, WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, bool $mapPropertyNames = true, + ?string $context = null, ): array { - return DataTransformer::create($transformValues, $wrapExecutionType, $mapPropertyNames)->transform($this); + return DataTransformer::create($transformValues, $wrapExecutionType, $mapPropertyNames, $context)->transform($this); } public function getMorphClass(): string diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 39b66ea9..e2218fb5 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -39,12 +39,14 @@ public function transform( bool $transformValues = true, WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, bool $mapPropertyNames = true, + ?string $context = null, ): array { $transformer = new DataCollectableTransformer( $this->dataClass, $transformValues, $wrapExecutionType, $mapPropertyNames, + $context, $this->getPartialTrees(), $this->items, $this->getWrap(), diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index c3703255..86c5a433 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -27,6 +27,7 @@ public function toResponse($request) return new JsonResponse( data: $this->transform( wrapExecutionType: WrapExecutionType::Enabled, + context: 'response' ), status: $this->calculateResponseStatus($request) ); diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 8d9777dc..c512e969 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -13,17 +13,22 @@ public function all(): array public function toArray(): array { - return $this->transform(); + return $this->transform(context: 'array'); } public function toJson($options = 0): string { - return json_encode($this->transform(), $options); + return json_encode($this->transform(context: 'json'), $options); + } + + public function toEloquent(): string + { + return json_encode($this->transform(context: 'eloquent')); } public function jsonSerialize(): array { - return $this->transform(); + return $this->transform(context: 'json'); } public static function castUsing(array $arguments) diff --git a/src/Contracts/TransformableData.php b/src/Contracts/TransformableData.php index d1127355..e0844023 100644 --- a/src/Contracts/TransformableData.php +++ b/src/Contracts/TransformableData.php @@ -14,6 +14,8 @@ public function all(): array; public function toArray(): array; + public function toEloquent(): string; + public function toJson($options = 0): string; public function jsonSerialize(): array; @@ -22,6 +24,7 @@ public function transform( bool $transformValues = true, WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, bool $mapPropertyNames = true, + ?string $context = null, ): array; public static function castUsing(array $arguments); diff --git a/src/Support/EloquentCasts/DataEloquentCast.php b/src/Support/EloquentCasts/DataEloquentCast.php index bc14fb5f..29541c2a 100644 --- a/src/Support/EloquentCasts/DataEloquentCast.php +++ b/src/Support/EloquentCasts/DataEloquentCast.php @@ -66,11 +66,11 @@ public function set($model, string $key, $value, array $attributes): ?string if ($isAbstractClassCast) { return json_encode([ 'type' => $this->dataConfig->morphMap->getDataClassAlias($value::class) ?? $value::class, - 'data' => json_decode($value->toJson(), associative: true, flags: JSON_THROW_ON_ERROR), + 'data' => json_decode($value->toEloquent(), associative: true, flags: JSON_THROW_ON_ERROR), ]); } - return $value->toJson(); + return $value->toEloquent(); } protected function isAbstractClassCast(): bool diff --git a/src/Transformers/ArrayableTransformer.php b/src/Transformers/ArrayableTransformer.php index 0dc4a545..a9968068 100644 --- a/src/Transformers/ArrayableTransformer.php +++ b/src/Transformers/ArrayableTransformer.php @@ -6,7 +6,7 @@ class ArrayableTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): array + public function transform(DataProperty $property, mixed $value, ?string $context = null): array { /** @var \Illuminate\Contracts\Support\Arrayable $value */ return $value->toArray(); diff --git a/src/Transformers/DataCollectableTransformer.php b/src/Transformers/DataCollectableTransformer.php index 5c3bdbb4..856d0df2 100644 --- a/src/Transformers/DataCollectableTransformer.php +++ b/src/Transformers/DataCollectableTransformer.php @@ -20,6 +20,7 @@ public function __construct( protected bool $transformValues, protected WrapExecutionType $wrapExecutionType, protected bool $mapPropertyNames, + protected ?string $context, protected PartialTrees $trees, protected Enumerable|CursorPaginator|Paginator $items, protected Wrap $wrap, @@ -33,7 +34,9 @@ public function transform(): array } $this->items->through( - $this->transformItemClosure() + fn (mixed $item) => $item instanceof BaseData + ? $this->transformItemClosure()($item)->transform(context: $this->context) + : $item ); return $this->transformValues @@ -60,6 +63,7 @@ protected function transformCollection(Enumerable $items): array ? WrapExecutionType::TemporarilyDisabled : $this->wrapExecutionType, $this->mapPropertyNames, + $this->context, ); } diff --git a/src/Transformers/DataTransformer.php b/src/Transformers/DataTransformer.php index 293ac3f0..1a130ca8 100644 --- a/src/Transformers/DataTransformer.php +++ b/src/Transformers/DataTransformer.php @@ -31,14 +31,16 @@ public static function create( bool $transformValues, WrapExecutionType $wrapExecutionType, bool $mapPropertyNames, + ?string $context = null, ): self { - return new self($transformValues, $wrapExecutionType, $mapPropertyNames); + return new self($transformValues, $wrapExecutionType, $mapPropertyNames, $context); } public function __construct( protected bool $transformValues, protected WrapExecutionType $wrapExecutionType, protected bool $mapPropertyNames, + protected ?string $context = null, ) { $this->config = app(DataConfig::class); } @@ -198,7 +200,7 @@ protected function isPropertyLazyIncluded( protected function resolvePropertyValue( DataProperty $property, mixed $value, - PartialTrees $trees + PartialTrees $trees, ): mixed { if ($value instanceof Lazy) { $value = $value->resolve(); @@ -208,17 +210,17 @@ protected function resolvePropertyValue( return null; } - if (is_array($value) && ($trees->only instanceof AllTreeNode || $trees->only instanceof PartialTreeNode)) { + if (\is_array($value) && ($trees->only instanceof AllTreeNode || $trees->only instanceof PartialTreeNode)) { $value = Arr::only($value, $trees->only->getFields()); } - if (is_array($value) && ($trees->except instanceof AllTreeNode || $trees->except instanceof PartialTreeNode)) { + if (\is_array($value) && ($trees->except instanceof AllTreeNode || $trees->except instanceof PartialTreeNode)) { $value = Arr::except($value, $trees->except->getFields()); } - if (count($transformers = $this->resolveTransformersForValue($property, $value))) { + if (\count($transformers = $this->resolveTransformersForValue($property, $value))) { foreach ($transformers as $transformer) { - if ($result = $transformer->transform($property, $value)) { + if ($result = $transformer->transform($property, $value, $this->context)) { return $result; } } @@ -243,12 +245,13 @@ protected function resolvePropertyValue( }; if ($value instanceof TransformableData && $this->transformValues) { - return $value->transform($this->transformValues, $wrapExecutionType, $this->mapPropertyNames, ); + return $value->transform($this->transformValues, $wrapExecutionType, $this->mapPropertyNames, $this->context); } return $value; } + /** @return Collection */ protected function resolveTransformersForValue( DataProperty $property, mixed $value, diff --git a/src/Transformers/DateTimeInterfaceTransformer.php b/src/Transformers/DateTimeInterfaceTransformer.php index 0f8340fe..4b2300da 100644 --- a/src/Transformers/DateTimeInterfaceTransformer.php +++ b/src/Transformers/DateTimeInterfaceTransformer.php @@ -14,7 +14,7 @@ public function __construct( ) { } - public function transform(DataProperty $property, mixed $value): string + public function transform(DataProperty $property, mixed $value, ?string $context = null): string { [$format] = Arr::wrap($this->format ?? config('data.date_format')); diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 9d30e0d8..aa5799a9 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -6,7 +6,7 @@ class EnumTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): string|int + public function transform(DataProperty $property, mixed $value, ?string $context = null): string|int { return $value->value; } diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php index 092b0014..d3abf964 100644 --- a/src/Transformers/Transformer.php +++ b/src/Transformers/Transformer.php @@ -6,5 +6,5 @@ interface Transformer { - public function transform(DataProperty $property, mixed $value): mixed; + public function transform(DataProperty $property, mixed $value, ?string $context = null): mixed; } diff --git a/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php b/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php index 0672bb49..9125b8c6 100644 --- a/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php +++ b/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php @@ -8,9 +8,9 @@ class ConfidentialDataCollectionTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): mixed + public function transform(DataProperty $property, mixed $value, ?string $context = null): mixed { /** @var \Spatie\LaravelData\DataCollection $value */ - return $value->toCollection()->map(fn (Data $data) => (new ConfidentialDataTransformer())->transform($property, $data))->toArray(); + return $value->toCollection()->map(fn (Data $data) => (new ConfidentialDataTransformer())->transform($property, $data, $context))->toArray(); } } diff --git a/tests/Fakes/Transformers/ConfidentialDataTransformer.php b/tests/Fakes/Transformers/ConfidentialDataTransformer.php index c47942ec..2e032ee4 100644 --- a/tests/Fakes/Transformers/ConfidentialDataTransformer.php +++ b/tests/Fakes/Transformers/ConfidentialDataTransformer.php @@ -9,7 +9,7 @@ class ConfidentialDataTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): mixed + public function transform(DataProperty $property, mixed $value, ?string $context = null): mixed { /** @var \Spatie\LaravelData\Data $value */ return collect($value->toArray())->map(fn (mixed $value) => 'CONFIDENTIAL')->toArray(); diff --git a/tests/Fakes/Transformers/StringToUpperTransformer.php b/tests/Fakes/Transformers/StringToUpperTransformer.php index 43208bab..c056b890 100644 --- a/tests/Fakes/Transformers/StringToUpperTransformer.php +++ b/tests/Fakes/Transformers/StringToUpperTransformer.php @@ -7,7 +7,7 @@ class StringToUpperTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): string + public function transform(DataProperty $property, mixed $value, ?string $context = null): string { return strtoupper($value); } diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index 2e6dd211..790bc8bd 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -64,7 +64,6 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbon')), new Carbon('19-05-1994 00:00:00'), - [] ) )->toEqual('19-05-1994'); @@ -72,7 +71,6 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), new CarbonImmutable('19-05-1994 00:00:00'), - [] ) )->toEqual('19-05-1994'); @@ -80,7 +78,6 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTime')), new DateTime('19-05-1994 00:00:00'), - [] ) )->toEqual('19-05-1994'); @@ -88,7 +85,6 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), new DateTimeImmutable('19-05-1994 00:00:00'), - [] ) )->toEqual('19-05-1994'); });