From c84dd510800341d22868d9af44071c3374ccb6d0 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 8 Jan 2024 13:33:42 +0100 Subject: [PATCH] Add support for transforms in context --- UPGRADING.md | 1 + src/Resolvers/TransformedDataResolver.php | 12 ++++- src/Resolvers/VisibleDataFieldsResolver.php | 1 + src/Support/DataConfig.php | 27 ++-------- .../GlobalTransformersCollection.php | 50 +++++++++++++++++++ .../Transformation/TransformationContext.php | 1 + .../TransformationContextFactory.php | 14 ++++++ src/Transformers/ArrayableTransformer.php | 4 +- .../DateTimeInterfaceTransformer.php | 6 ++- src/Transformers/EnumTransformer.php | 3 +- src/Transformers/Transformer.php | 7 ++- tests/DataTest.php | 29 ++++++++++- .../ConfidentialDataCollectionTransformer.php | 5 +- .../ConfidentialDataTransformer.php | 3 +- .../Transformers/StringToUpperTransformer.php | 7 ++- .../DateTimeInterfaceTransformerTest.php | 45 ++++++++++------- tests/Transformers/EnumTransformerTest.php | 7 ++- 17 files changed, 168 insertions(+), 54 deletions(-) create mode 100644 src/Support/Transformation/GlobalTransformersCollection.php diff --git a/UPGRADING.md b/UPGRADING.md index 53f8e87ee..afac760b9 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -17,6 +17,7 @@ The following things are required when upgrading: - EmptyData (T/I) and ContextableData (T/I) was added - If you were calling the transform method on a data object, a `TransformationContextFactory` or `TransformationContext` is now the only parameter you can pass - Take a look within the docs what has changed +- If you have implemented a custom `Transformer`, update the `transform` method signature with the new `TransformationContext` parameter - If you were using internal data structures like `DataClass` and `DataProperty` then take a look at what has been changed - The `DataCollectableTransformer` and `DataTransformer` were replaced with their appropriate resolvers - If you've cached the data structures, be sure to clear the cache diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 7b359f824..145b7c719 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -94,7 +94,7 @@ protected function resolvePropertyValue( } if ($transformer = $this->resolveTransformerForValue($property, $value, $currentContext)) { - return $transformer->transform($property, $value); + return $transformer->transform($property, $value, $currentContext); } if (is_array($value) && ! $property->type->kind->isDataCollectable()) { @@ -197,7 +197,15 @@ protected function resolveTransformerForValue( return null; } - $transformer = $property->transformer ?? $this->dataConfig->findGlobalTransformerForValue($value); + $transformer = $property->transformer; + + if ($transformer === null && $context->transformers) { + $transformer = $context->transformers->findTransformerForValue($value); + } + + if ($transformer === null) { + $transformer = $this->dataConfig->transformers->findTransformerForValue($value); + } $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer && $property->type->kind !== DataTypeKind::Default; diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index 6a6eba638..9d71dda72 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -39,6 +39,7 @@ public function execute( $transformationContext->transformValues, $transformationContext->mapPropertyNames, $transformationContext->wrapExecutionType, + $transformationContext->transformers, ); } } diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index dd6c2214d..3d59e6c85 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -5,6 +5,7 @@ use ReflectionClass; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Support\Transformation\GlobalTransformersCollection; use Spatie\LaravelData\Transformers\Transformer; class DataConfig @@ -12,8 +13,7 @@ class DataConfig /** @var array */ protected array $dataClasses = []; - /** @var array */ - protected array $transformers = []; + public GlobalTransformersCollection $transformers; /** @var array */ protected array $casts = []; @@ -33,8 +33,10 @@ public function __construct(array $config) $config['rule_inferrers'] ?? [] ); + $this->transformers = new GlobalTransformersCollection(); + foreach ($config['transformers'] ?? [] as $transformable => $transformer) { - $this->transformers[ltrim($transformable, ' \\')] = app($transformer); + $this->transformers->add($transformable, app($transformer)); } foreach ($config['casts'] ?? [] as $castable => $cast) { @@ -75,25 +77,6 @@ public function findGlobalCastForProperty(DataProperty $property): ?Cast return null; } - public function findGlobalTransformerForValue(mixed $value): ?Transformer - { - if (gettype($value) !== 'object') { - return null; - } - - foreach ($this->transformers as $transformable => $transformer) { - if ($value::class === $transformable) { - return $transformer; - } - - if (is_a($value::class, $transformable, true)) { - return $transformer; - } - } - - return null; - } - public function getRuleInferrers(): array { return $this->ruleInferrers; diff --git a/src/Support/Transformation/GlobalTransformersCollection.php b/src/Support/Transformation/GlobalTransformersCollection.php new file mode 100644 index 000000000..598456fcd --- /dev/null +++ b/src/Support/Transformation/GlobalTransformersCollection.php @@ -0,0 +1,50 @@ + $transformers + */ + public function __construct( + protected array $transformers = [] + ) { + } + + public function add(string $transformable, Transformer $transformer): self + { + $this->transformers[ltrim($transformable, ' \\')] = $transformer; + + return $this; + } + + public function findTransformerForValue(mixed $value): ?Transformer + { + if (gettype($value) !== 'object') { + return null; + } + + foreach ($this->transformers as $transformable => $transformer) { + if ($value::class === $transformable) { + return $transformer; + } + + if (is_a($value::class, $transformable, true)) { + return $transformer; + } + } + + return null; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->transformers); + } +} diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index e71562cc5..8cff2bb72 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -16,6 +16,7 @@ public function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, + public ?GlobalTransformersCollection $transformers = null, public ?ResolvedPartialsCollection $includedPartials = null, public ?ResolvedPartialsCollection $excludedPartials = null, public ?ResolvedPartialsCollection $onlyPartials = null, diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 898212156..d1ec4f21e 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Support\Partials\PartialsCollection; use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; +use Spatie\LaravelData\Transformers\Transformer; class TransformationContextFactory { @@ -23,6 +24,7 @@ protected function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, + public ?GlobalTransformersCollection $transformers = null, public ?PartialsCollection $includedPartials = null, public ?PartialsCollection $excludedPartials = null, public ?PartialsCollection $onlyPartials = null, @@ -93,6 +95,7 @@ public function get( $this->transformValues, $this->mapPropertyNames, $this->wrapExecutionType, + $this->transformers, $includedPartials, $excludedPartials, $onlyPartials, @@ -121,6 +124,17 @@ public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static return $this; } + public function transformer(string $transformable, Transformer $transformer): static + { + if ($this->transformers === null) { + $this->transformers = new GlobalTransformersCollection(); + } + + $this->transformers->add($transformable, $transformer); + + return $this; + } + public function addIncludePartial(Partial ...$partial): static { if ($this->includedPartials === null) { diff --git a/src/Transformers/ArrayableTransformer.php b/src/Transformers/ArrayableTransformer.php index 0dc4a545d..766d218e0 100644 --- a/src/Transformers/ArrayableTransformer.php +++ b/src/Transformers/ArrayableTransformer.php @@ -2,11 +2,13 @@ namespace Spatie\LaravelData\Transformers; +use Illuminate\Contracts\Support\Arrayable; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; class ArrayableTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): array + public function transform(DataProperty $property, mixed $value, TransformationContext $context): array { /** @var \Illuminate\Contracts\Support\Arrayable $value */ return $value->toArray(); diff --git a/src/Transformers/DateTimeInterfaceTransformer.php b/src/Transformers/DateTimeInterfaceTransformer.php index 19ad2745b..5781d3b6f 100644 --- a/src/Transformers/DateTimeInterfaceTransformer.php +++ b/src/Transformers/DateTimeInterfaceTransformer.php @@ -2,9 +2,11 @@ namespace Spatie\LaravelData\Transformers; +use DateTimeInterface; use DateTimeZone; use Illuminate\Support\Arr; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; class DateTimeInterfaceTransformer implements Transformer { @@ -17,9 +19,9 @@ public function __construct( [$this->format] = Arr::wrap($format ?? config('data.date_format')); } - public function transform(DataProperty $property, mixed $value): string + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string { - /** @var \DateTimeInterface $value */ + /** @var DateTimeInterface $value */ if ($this->setTimeZone) { $value = (clone $value)->setTimezone(new DateTimeZone($this->setTimeZone)); } diff --git a/src/Transformers/EnumTransformer.php b/src/Transformers/EnumTransformer.php index 9d30e0d89..afdc32eba 100644 --- a/src/Transformers/EnumTransformer.php +++ b/src/Transformers/EnumTransformer.php @@ -3,10 +3,11 @@ namespace Spatie\LaravelData\Transformers; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; class EnumTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): string|int + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string|int { return $value->value; } diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php index 092b00143..6ef510f3f 100644 --- a/src/Transformers/Transformer.php +++ b/src/Transformers/Transformer.php @@ -3,8 +3,13 @@ namespace Spatie\LaravelData\Transformers; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; interface Transformer { - public function transform(DataProperty $property, mixed $value): mixed; + public function transform( + DataProperty $property, + mixed $value, + TransformationContext $context + ): mixed; } diff --git a/tests/DataTest.php b/tests/DataTest.php index 63e578607..a923ce769 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -33,6 +33,8 @@ use Spatie\LaravelData\Exceptions\CannotSetComputedValue; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Fakes\Castables\SimpleCastable; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCast; @@ -56,8 +58,8 @@ use Spatie\LaravelData\Tests\Fakes\Transformers\StringToUpperTransformer; use Spatie\LaravelData\Tests\Fakes\UlarData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; +use Spatie\LaravelData\Transformers\Transformer; use Spatie\LaravelData\WithData; - use function Spatie\Snapshots\assertMatchesSnapshot; it('can create a resource', function () { @@ -1527,3 +1529,28 @@ public function __construct( yield 'no params' => [[], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 0 given. Parameters missing: first, second.'], yield 'one param' => [['first' => 'First'], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 1 given. Parameters given: first. Parameters missing: second.'], ]); + +it('is possible to add extra global transformers when transforming using context', function () { + $dataClass = new class extends Data { + public DateTime $dateTime; + }; + + $data = $dataClass::from([ + 'dateTime' => new DateTime(), + ]); + + $customTransformer = new class implements Transformer { + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string + { + return "Custom transformed date"; + } + }; + + $transformed = $data->transform( + TransformationContextFactory::create()->transformer(DateTimeInterface::class, $customTransformer) + ); + + expect($transformed)->toBe([ + 'dateTime' => 'Custom transformed date', + ]); +}); diff --git a/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php b/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php index 15344b273..96e25035b 100644 --- a/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php +++ b/tests/Fakes/Transformers/ConfidentialDataCollectionTransformer.php @@ -4,13 +4,14 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Transformers\Transformer; class ConfidentialDataCollectionTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): mixed + public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed { /** @var array $value */ - return array_map(fn (Data $data) => (new ConfidentialDataTransformer())->transform($property, $data), $value); + return array_map(fn (Data $data) => (new ConfidentialDataTransformer())->transform($property, $data, $context), $value); } } diff --git a/tests/Fakes/Transformers/ConfidentialDataTransformer.php b/tests/Fakes/Transformers/ConfidentialDataTransformer.php index c47942ec0..85a99631a 100644 --- a/tests/Fakes/Transformers/ConfidentialDataTransformer.php +++ b/tests/Fakes/Transformers/ConfidentialDataTransformer.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData\Tests\Fakes\Transformers; +use Spatie\LaravelData\Support\Transformation\TransformationContext; use function collect; use Spatie\LaravelData\Support\DataProperty; @@ -9,7 +10,7 @@ class ConfidentialDataTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): mixed + public function transform(DataProperty $property, mixed $value, TransformationContext $context): 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 43208bab1..66e6ad713 100644 --- a/tests/Fakes/Transformers/StringToUpperTransformer.php +++ b/tests/Fakes/Transformers/StringToUpperTransformer.php @@ -3,12 +3,15 @@ namespace Spatie\LaravelData\Tests\Fakes\Transformers; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Transformers\Transformer; class StringToUpperTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): string + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string { return strtoupper($value); } -}; +} + +; diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index d50eacc5e..e6b5162fb 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -2,13 +2,15 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; +use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; it('can transform dates', function () { $transformer = new DateTimeInterfaceTransformer(); - $class = new class () { + $class = new class () extends Data{ public Carbon $carbon; public CarbonImmutable $carbonImmutable; @@ -21,28 +23,32 @@ expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbon')), - new Carbon('19-05-1994 00:00:00') + new Carbon('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T00:00:00+00:00'); expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), - new CarbonImmutable('19-05-1994 00:00:00') + new CarbonImmutable('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T00:00:00+00:00'); expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTime')), - new DateTime('19-05-1994 00:00:00') + new DateTime('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T00:00:00+00:00'); expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), - new DateTimeImmutable('19-05-1994 00:00:00') + new DateTimeImmutable('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T00:00:00+00:00'); }); @@ -50,7 +56,7 @@ it('can transform dates with an alternative format', function () { $transformer = new DateTimeInterfaceTransformer(format: 'd-m-Y'); - $class = new class () { + $class = new class () extends Data{ public Carbon $carbon; public CarbonImmutable $carbonImmutable; @@ -64,7 +70,7 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbon')), new Carbon('19-05-1994 00:00:00'), - [] + TransformationContextFactory::create()->get($class) ) )->toEqual('19-05-1994'); @@ -72,7 +78,7 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), new CarbonImmutable('19-05-1994 00:00:00'), - [] + TransformationContextFactory::create()->get($class) ) )->toEqual('19-05-1994'); @@ -80,7 +86,7 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTime')), new DateTime('19-05-1994 00:00:00'), - [] + TransformationContextFactory::create()->get($class) ) )->toEqual('19-05-1994'); @@ -88,7 +94,7 @@ $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), new DateTimeImmutable('19-05-1994 00:00:00'), - [] + TransformationContextFactory::create()->get($class) ) )->toEqual('19-05-1994'); }); @@ -96,7 +102,7 @@ it('can change the timezone', function () { $transformer = new DateTimeInterfaceTransformer(setTimeZone: 'Europe/Brussels'); - $class = new class () { + $class = new class () extends Data { public Carbon $carbon; public CarbonImmutable $carbonImmutable; @@ -109,28 +115,32 @@ expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbon')), - new Carbon('19-05-1994 00:00:00') + new Carbon('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T02:00:00+02:00'); expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), - new CarbonImmutable('19-05-1994 00:00:00') + new CarbonImmutable('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T02:00:00+02:00'); expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTime')), - new DateTime('19-05-1994 00:00:00') + new DateTime('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T02:00:00+02:00'); expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), - new DateTimeImmutable('19-05-1994 00:00:00') + new DateTimeImmutable('19-05-1994 00:00:00'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19T02:00:00+02:00'); }); @@ -140,14 +150,15 @@ $transformer = new DateTimeInterfaceTransformer(); - $class = new class () { + $class = new class () extends Data { public Carbon $carbon; }; expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbon')), - Carbon::createFromFormat('!Y-m-d', '1994-05-19') + Carbon::createFromFormat('!Y-m-d', '1994-05-19'), + TransformationContextFactory::create()->get($class) ) )->toEqual('1994-05-19'); }); diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php index c3db2599e..32784cd01 100644 --- a/tests/Transformers/EnumTransformerTest.php +++ b/tests/Transformers/EnumTransformerTest.php @@ -1,20 +1,23 @@ transform( DataProperty::create(new ReflectionProperty($class, 'enum')), - $class->enum + $class->enum, + TransformationContextFactory::create()->get($class) ) )->toEqual(DummyBackedEnum::FOO->value); });