From e1e491d20f09b0e07a7199487926c76741d7d39f Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 14 Feb 2023 16:15:07 +0100 Subject: [PATCH 001/124] Allow accessors to be used when creating data from models --- src/Normalizers/ModelNormalizer.php | 4 ++ tests/Fakes/FakeModelData.php | 3 ++ tests/Fakes/Models/FakeModel.php | 11 ++++++ tests/Normalizers/ModelNormalizerTest.php | 47 +++-------------------- 4 files changed, 24 insertions(+), 41 deletions(-) diff --git a/src/Normalizers/ModelNormalizer.php b/src/Normalizers/ModelNormalizer.php index df52d369..a7a278cd 100644 --- a/src/Normalizers/ModelNormalizer.php +++ b/src/Normalizers/ModelNormalizer.php @@ -31,6 +31,10 @@ public function normalize(mixed $value): ?array $properties[$key] = $relation; } + foreach ($value->getMutatedAttributes() as $key) { + $properties[$key] = $value->getAttribute($key); + } + return $properties; } diff --git a/tests/Fakes/FakeModelData.php b/tests/Fakes/FakeModelData.php index b2209aae..9969e1e1 100644 --- a/tests/Fakes/FakeModelData.php +++ b/tests/Fakes/FakeModelData.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Tests\Fakes; use Carbon\CarbonImmutable; +use Illuminate\Database\Eloquent\Casts\Attribute; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; @@ -16,6 +17,8 @@ public function __construct( public CarbonImmutable $date, #[DataCollectionOf(FakeNestedModelData::class)] public Optional|null|DataCollection $fake_nested_models, + public string $accessor, + public string $old_accessor, ) { } } diff --git a/tests/Fakes/Models/FakeModel.php b/tests/Fakes/Models/FakeModel.php index efbbac61..6849dc11 100644 --- a/tests/Fakes/Models/FakeModel.php +++ b/tests/Fakes/Models/FakeModel.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData\Tests\Fakes\Models; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -20,6 +21,16 @@ public function fakeNestedModels(): HasMany return $this->hasMany(FakeNestedModel::class); } + public function accessor(): Attribute + { + return Attribute::get(fn() => "accessor_{$this->string}"); + } + + public function getOldAccessorAttribute() + { + return "old_accessor_{$this->string}"; + } + protected static function newFactory() { return FakeModelFactory::new(); diff --git a/tests/Normalizers/ModelNormalizerTest.php b/tests/Normalizers/ModelNormalizerTest.php index b1c2a3af..f2bf30b5 100644 --- a/tests/Normalizers/ModelNormalizerTest.php +++ b/tests/Normalizers/ModelNormalizerTest.php @@ -43,46 +43,11 @@ ->date->toEqual($data->fake_nested_models[1]->date); }); -it('can get a data object from model with dates', function () { - $fakeModelClass = new class () extends Model { - protected $casts = [ - 'date' => 'date', - 'datetime' => 'datetime', - 'immutable_date' => 'immutable_date', - 'immutable_datetime' => 'immutable_datetime', - ]; - }; - - $model = $fakeModelClass::make([ - 'date' => Carbon::create(2020, 05, 16, 12, 00, 00), - 'datetime' => Carbon::create(2020, 05, 16, 12, 00, 00), - 'immutable_date' => Carbon::create(2020, 05, 16, 12, 00, 00), - 'immutable_datetime' => Carbon::create(2020, 05, 16, 12, 00, 00), - 'created_at' => Carbon::create(2020, 05, 16, 12, 00, 00), - 'updated_at' => Carbon::create(2020, 05, 16, 12, 00, 00), - ]); - - class TestDataFromModelWithDates extends Data - { - public function __construct( - public Carbon $date, - public Carbon $datetime, - public CarbonImmutable $immutable_date, - public CarbonImmutable $immutable_datetime, - public Carbon $created_at, - public Carbon $updated_at, - ) { - } - } - - $data = \TestDataFromModelWithDates::from($model); +it('can get a data object from model with accessors', function () { + $model = FakeModel::factory()->create(); + $data = FakeModelData::from($model); - expect([ - $data->date->eq(Carbon::create(2020, 05, 16, 00, 00, 00)), - $data->datetime->eq(Carbon::create(2020, 05, 16, 12, 00, 00)), - $data->immutable_date->eq(Carbon::create(2020, 05, 16, 00, 00, 00)), - $data->immutable_datetime->eq(Carbon::create(2020, 05, 16, 12, 00, 00)), - $data->created_at->eq(Carbon::create(2020, 05, 16, 12, 00, 00)), - $data->updated_at->eq(Carbon::create(2020, 05, 16, 12, 00, 00)), - ])->each->toBeTrue(); + expect($model) + ->accessor->toEqual($data->accessor) + ->old_accessor->toEqual($data->old_accessor); }); From 3b5f29fa45b6a95b95c7ec370da03fb4465a940e Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 16 Feb 2023 10:48:30 +0100 Subject: [PATCH 002/124] Refactor partial trees --- src/Concerns/BaseData.php | 45 +++- src/Concerns/BaseDataCollectable.php | 44 +++ src/Concerns/IncludeableData.php | 8 + src/Concerns/ResponsableData.php | 26 +- src/Concerns/TransformableData.php | 10 +- src/Concerns/WrappableData.php | 2 + src/Contracts/BaseData.php | 3 + .../TransformedDataCollectionResolver.php | 132 +++++++++ src/Resolvers/TransformedDataResolver.php | 254 ++++++++++++++++++ src/Support/Transformation/DataContext.php | 25 ++ .../LocalTransformationContext.php | 51 ++++ .../Transformation/TransformationContext.php | 67 +++++ .../TransformationContextFactory.php | 139 ++++++++++ .../Transforming/TransformationContext.php | 24 ++ src/Transformers/DataTransformer.php | 4 - tests/DataTest.php | 114 ++++---- tests/Datasets/DataTest.php | 8 +- tests/Normalizers/JsonNormalizerTest.php | 2 +- 18 files changed, 870 insertions(+), 88 deletions(-) create mode 100644 src/Resolvers/TransformedDataCollectionResolver.php create mode 100644 src/Resolvers/TransformedDataResolver.php create mode 100644 src/Support/Transformation/DataContext.php create mode 100644 src/Support/Transformation/LocalTransformationContext.php create mode 100644 src/Support/Transformation/TransformationContext.php create mode 100644 src/Support/Transformation/TransformationContextFactory.php create mode 100644 src/Support/Transforming/TransformationContext.php diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index be969a7a..3b1e98a5 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -7,6 +7,8 @@ use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; use Illuminate\Support\Enumerable; +use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; +use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; @@ -19,9 +21,16 @@ use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Resolvers\EmptyDataResolver; +use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Transformation\DataContext; +use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; +use Spatie\LaravelData\Support\Wrapping\WrapType; use Spatie\LaravelData\Transformers\DataTransformer; trait BaseData @@ -32,6 +41,8 @@ trait BaseData protected static string $_cursorPaginatedCollectionClass = CursorPaginatedDataCollection::class; + protected ?DataContext $_dataContext = null; + public static function optional(mixed ...$payloads): ?static { if (count($payloads) === 0) { @@ -106,12 +117,44 @@ public function transform( return DataTransformer::create($transformValues, $wrapExecutionType, $mapPropertyNames)->transform($this); } + public function transform2( + null|TransformationContextFactory|TransformationContext $context = null, + ): array { + if ($context === null) { + $context = new TransformationContext(); + } + + if ($context instanceof TransformationContextFactory) { + $context = $context->get(); + } + + return app(TransformedDataResolver::class)->execute( + $this, + $context->merge(LocalTransformationContext::create($this)) + ); + } + public function __sleep(): array { return app(DataConfig::class)->getDataClass(static::class) ->properties - ->map(fn (DataProperty $property) => $property->name) + ->map(fn(DataProperty $property) => $property->name) ->push('_additional') ->toArray(); } + + public function getDataContext(): DataContext + { + if ($this->_dataContext === null) { + return $this->_dataContext = new DataContext( + $this instanceof IncludeableDataContract ? $this->includeProperties() : [], + $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], + $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], + $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + $this instanceof WrappableDataContract ? $this->getWrap() : new Wrap(WrapType::UseGlobal), + ); + } + + return $this->_dataContext; + } } diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 39b66ea9..83747fb6 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -3,7 +3,17 @@ namespace Spatie\LaravelData\Concerns; use ArrayIterator; +use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; +use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; +use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; +use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Support\Transformation\DataContext; +use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; +use Spatie\LaravelData\Support\Wrapping\WrapType; use Spatie\LaravelData\Transformers\DataCollectableTransformer; /** @@ -12,6 +22,8 @@ */ trait BaseDataCollectable { + protected ?DataContext $_dataContext = null; + /** @return class-string */ public function getDataClass(): string { @@ -53,8 +65,40 @@ public function transform( return $transformer->transform(); } + public function transform2( + null|TransformationContextFactory|TransformationContext $context = null, + ): array { + if ($context === null) { + $context = new TransformationContext(); + } + + if ($context instanceof TransformationContextFactory) { + $context = $context->get(); + } + + return app(TransformedDataCollectionResolver::class)->execute( + $this, + $context->merge(LocalTransformationContext::create($this)) + ); + } + public function __sleep(): array { return ['items', 'dataClass']; } + + public function getDataContext(): DataContext + { + if ($this->_dataContext === null) { + return $this->_dataContext = new DataContext( + $this instanceof IncludeableDataContract ? $this->includeProperties() : [], + $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], + $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], + $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + $this instanceof WrappableDataContract ? $this->getWrap() : new Wrap(WrapType::UseGlobal), + ); + } + + return $this->_dataContext; + } } diff --git a/src/Concerns/IncludeableData.php b/src/Concerns/IncludeableData.php index 69a8d2ed..228cbc26 100644 --- a/src/Concerns/IncludeableData.php +++ b/src/Concerns/IncludeableData.php @@ -32,6 +32,7 @@ public function withPartialTrees(PartialTrees $partialTrees): static public function include(string ...$includes): static { foreach ($includes as $include) { + $this->getDataContext()->includes[$include] = true; $this->_includes[$include] = true; } @@ -41,6 +42,7 @@ public function include(string ...$includes): static public function exclude(string ...$excludes): static { foreach ($excludes as $exclude) { + $this->getDataContext()->excludes[$exclude] = true; $this->_excludes[$exclude] = true; } @@ -50,6 +52,7 @@ public function exclude(string ...$excludes): static public function only(string ...$only): static { foreach ($only as $onlyDefinition) { + $this->getDataContext()->only[$onlyDefinition] = true; $this->_only[$onlyDefinition] = true; } @@ -59,6 +62,7 @@ public function only(string ...$only): static public function except(string ...$except): static { foreach ($except as $exceptDefinition) { + $this->getDataContext()->except[$exceptDefinition] = true; $this->_except[$exceptDefinition] = true; } @@ -67,6 +71,7 @@ public function except(string ...$except): static public function includeWhen(string $include, bool|Closure $condition): static { + $this->getDataContext()->includes[$include] = $condition; $this->_includes[$include] = $condition; return $this; @@ -74,6 +79,7 @@ public function includeWhen(string $include, bool|Closure $condition): static public function excludeWhen(string $exclude, bool|Closure $condition): static { + $this->getDataContext()->excludes[$exclude] = $condition; $this->_excludes[$exclude] = $condition; return $this; @@ -81,6 +87,7 @@ public function excludeWhen(string $exclude, bool|Closure $condition): static public function onlyWhen(string $only, bool|Closure $condition): static { + $this->getDataContext()->only[$only] = $condition; $this->_only[$only] = $condition; return $this; @@ -88,6 +95,7 @@ public function onlyWhen(string $only, bool|Closure $condition): static public function exceptWhen(string $except, bool|Closure $condition): static { + $this->getDataContext()->except[$except] = $condition; $this->_except[$except] = $condition; return $this; diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 11c1f8ea..873bfbd3 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -7,6 +7,10 @@ use Illuminate\Http\Response; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Resolvers\PartialsTreeFromRequestResolver; +use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; trait ResponsableData @@ -18,25 +22,27 @@ trait ResponsableData */ public function toResponse($request) { + $context = new TransformationContext( + wrapExecutionType: WrapExecutionType::Enabled, + ); + if ($this instanceof IncludeableDataContract) { $partialTrees = resolve(PartialsTreeFromRequestResolver::class)->execute($this, $request); - $this->withPartialTrees($partialTrees); + $context = $context->merge(new LocalTransformationContext( + $partialTrees->lazyIncluded, + $partialTrees->lazyExcluded, + $partialTrees->only, + $partialTrees->except, + )); } return new JsonResponse( - data: $this->transform( - wrapExecutionType: WrapExecutionType::Enabled, - ), - status: $this->calculateResponseStatus($request) + data: $this->transform2($context), + status: $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK, ); } - protected function calculateResponseStatus(Request $request): int - { - return $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK; - } - public static function allowedRequestIncludes(): ?array { return []; diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 112097c3..8fb48137 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -3,27 +3,29 @@ namespace Spatie\LaravelData\Concerns; use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast; +use Spatie\LaravelData\Support\Transformation\TransformationContext; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; trait TransformableData { public function all(): array { - return $this->transform(transformValues: false); + return $this->transform2(TransformationContextFactory::create()->transformValues(false)); } public function toArray(): array { - return $this->transform(); + return $this->transform2(); } public function toJson($options = 0): string { - return json_encode($this->transform(), $options); + return json_encode($this->transform2(), $options); } public function jsonSerialize(): array { - return $this->transform(); + return $this->transform2(); } public static function castUsing(array $arguments) diff --git a/src/Concerns/WrappableData.php b/src/Concerns/WrappableData.php index 89745028..e0bf0c0f 100644 --- a/src/Concerns/WrappableData.php +++ b/src/Concerns/WrappableData.php @@ -11,6 +11,7 @@ trait WrappableData public function withoutWrapping(): static { + $this->getDataContext()->wrap = new Wrap(WrapType::Disabled); $this->_wrap = new Wrap(WrapType::Disabled); return $this; @@ -18,6 +19,7 @@ public function withoutWrapping(): static public function wrap(string $key): static { + $this->getDataContext()->wrap = new Wrap(WrapType::Defined, $key); $this->_wrap = new Wrap(WrapType::Defined, $key); return $this; diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 9070fe1d..003fb0af 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -11,6 +11,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Transformation\DataContext; /** * @template TValue @@ -35,4 +36,6 @@ public static function normalizers(): array; public static function pipeline(): DataPipeline; public static function empty(array $extra = []): array; + + public function getDataContext(): DataContext; } diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php new file mode 100644 index 00000000..f724b30d --- /dev/null +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -0,0 +1,132 @@ +getWrap() + : new Wrap(WrapType::UseGlobal); + + $nestedContext = $context->wrapExecutionType->shouldExecute() + ? $context->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) + : $context; + + if ($items instanceof DataCollection) { + return $this->transformItems($items->items(), $wrap, $context, $nestedContext); + } + + if ($items instanceof Enumerable || is_array($items)) { + return $this->transformItems($items, $wrap, $context, $nestedContext); + } + + if ($items instanceof PaginatedDataCollection || $items instanceof CursorPaginatedDataCollection) { + return $this->transformPaginator($items->items(), $wrap, $context, $nestedContext); + } + + if ($items instanceof Paginator || $items instanceof CursorPaginator) { + return $this->transformPaginator($items, $wrap, $context, $nestedContext); + } + + throw new Exception("Cannot transform collection"); + } + + protected function transformItems( + Enumerable|array $items, + Wrap $wrap, + TransformationContext $context, + TransformationContext $nestedContext, + ): array { + $collection = []; + + foreach ($items as $key => $value) { + $collection[$key] = $this->transformationClosure($nestedContext)($value); + } + + return $context->wrapExecutionType->shouldExecute() + ? $wrap->wrap($collection) + : $collection; + } + + protected function transformPaginator( + Paginator|CursorPaginator $paginator, + Wrap $wrap, + TransformationContext $context, + TransformationContext $nestedContext, + ): array { + $paginator->through(fn(BaseData $data) => $this->transformationClosure($nestedContext)($data)); + + if ($context->transformValues === false) { + return $paginator->all(); + } + + $paginated = $paginator->toArray(); + + $wrapKey = $wrap->getKey() ?? 'data'; + + return [ + $wrapKey => $paginated['data'], + 'links' => $paginated['links'] ?? [], + 'meta' => Arr::except($paginated, [ + 'data', + 'links', + ]), + ]; + } + + protected function transformationClosure( + TransformationContext $context, + ): Closure { + return function (BaseData $data) use ($context) { + if (! $data instanceof TransformableData || ! $context->transformValues) { + return $data; + } + + return app(TransformedDataResolver::class)->execute($data, $context); + }; + } +} diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php new file mode 100644 index 00000000..5b9963b1 --- /dev/null +++ b/src/Resolvers/TransformedDataResolver.php @@ -0,0 +1,254 @@ +transform($data, $context); + + if ($data instanceof WrappableData && $context->wrapExecutionType->shouldExecute()) { + $transformed = $data->getWrap()->wrap($transformed); + } + + if ($data instanceof AppendableData) { + $transformed = array_merge($transformed, $data->getAdditionalData()); + } + + return $transformed; + } + + private function transform(BaseData&TransformableData $data, TransformationContext $context): array + { + return $this->dataConfig + ->getDataClass($data::class) + ->properties + ->reduce(function (array $payload, DataProperty $property) use ($data, $context) { + $name = $property->name; + + if (! $this->shouldIncludeProperty($name, $data->{$name}, $context)) { + return $payload; + } + + $value = $this->resolvePropertyValue( + $property, + $data->{$name}, + $context, + ); + + if ($context->mapPropertyNames && $property->outputMappedName) { + $name = $property->outputMappedName; + } + + $payload[$name] = $value; + + return $payload; + }, []); + } + + protected function shouldIncludeProperty( + string $name, + mixed $value, + TransformationContext $context + ): bool { + if ($value instanceof Optional) { + return false; + } + + if ($this->isPropertyHidden($name, $context)) { + return false; + } + + if (! $value instanceof Lazy) { + return true; + } + + if ($value instanceof RelationalLazy || $value instanceof ConditionalLazy) { + return $value->shouldBeIncluded(); + } + + // Lazy excluded checks + + if ($context->lazyExcluded instanceof AllTreeNode) { + return false; + } + + if ($context->lazyExcluded instanceof PartialTreeNode && $context->lazyExcluded->hasField($name)) { + return false; + } + + // Lazy included checks + + if ($context->lazyIncluded instanceof AllTreeNode) { + return true; + } + + if ($value->isDefaultIncluded()) { + return true; + } + + return $context->lazyIncluded instanceof PartialTreeNode && $context->lazyIncluded->hasField($name); + } + + protected function isPropertyHidden( + string $name, + TransformationContext $context + ): bool { + if ($context->except instanceof AllTreeNode) { + return true; + } + + if ( + $context->except instanceof PartialTreeNode + && $context->except->hasField($name) + && $context->except->getNested($name) instanceof ExcludedTreeNode + ) { + return true; + } + + if ($context->except instanceof PartialTreeNode) { + return false; + } + + if ($context->only instanceof AllTreeNode) { + return false; + } + + if ($context->only instanceof PartialTreeNode && $context->only->hasField($name)) { + return false; + } + + if ($context->only instanceof PartialTreeNode || $context->only instanceof ExcludedTreeNode) { + return true; + } + + return false; + } + + protected function resolvePropertyValue( + DataProperty $property, + mixed $value, + TransformationContext $context, + ): mixed { + if ($value instanceof Lazy) { + $value = $value->resolve(); + } + + if ($value === null) { + return null; + } + + $nextContext = $context->next($property->name); + + if (is_array($value) && ($nextContext->only instanceof AllTreeNode || $nextContext->only instanceof PartialTreeNode)) { + return Arr::only($value, $nextContext->only->getFields()); + } + + if (is_array($value) && ($nextContext->except instanceof AllTreeNode || $nextContext->except instanceof PartialTreeNode)) { + return Arr::except($value, $nextContext->except->getFields()); + } + + if ($transformer = $this->resolveTransformerForValue($property, $value, $nextContext)) { + return $transformer->transform($property, $value); + } + + if ( + $value instanceof BaseDataCollectable + && $value instanceof TransformableData + && $nextContext->transformValues + ) { + $wrapExecutionType = match ($context->wrapExecutionType){ + WrapExecutionType::Enabled => WrapExecutionType::Enabled, + WrapExecutionType::Disabled => WrapExecutionType::Disabled, + WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled + }; + + return $value->transform2($nextContext->setWrapExecutionType($wrapExecutionType)); + } + + if ( + $value instanceof BaseData + && $value instanceof TransformableData + && $nextContext->transformValues + ) { + $wrapExecutionType = match ($context->wrapExecutionType){ + WrapExecutionType::Enabled => WrapExecutionType::TemporarilyDisabled, + WrapExecutionType::Disabled => WrapExecutionType::Disabled, + WrapExecutionType::TemporarilyDisabled => WrapExecutionType::TemporarilyDisabled + }; + + return $value->transform2($nextContext->setWrapExecutionType($wrapExecutionType)); + } + + if ( + $property->type->isDataCollectable + && is_iterable($value) + && $nextContext->transformValues + ) { + $wrapExecutionType = match ($context->wrapExecutionType){ + WrapExecutionType::Enabled => WrapExecutionType::Enabled, + WrapExecutionType::Disabled => WrapExecutionType::Disabled, + WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled + }; + + return app(TransformedDataCollectionResolver::class)->execute( + $value, + $nextContext->setWrapExecutionType($wrapExecutionType) + ); + } + return $value; + } + + protected function resolveTransformerForValue( + DataProperty $property, + mixed $value, + TransformationContext $context, + ): ?Transformer { + if (! $context->transformValues) { + return null; + } + + $transformer = $property->transformer ?? $this->dataConfig->findGlobalTransformerForValue($value); + + $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer + && ($property->type->isDataObject || $property->type->isDataCollectable); + + if ($shouldUseDefaultDataTransformer) { + return null; + } + + return $transformer; + } +} diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php new file mode 100644 index 00000000..53db86bd --- /dev/null +++ b/src/Support/Transformation/DataContext.php @@ -0,0 +1,25 @@ + $includes + * @param array $excludes + * @param array $only + * @param array $except + * @param \Spatie\LaravelData\Support\Wrapping\Wrap|null $wrap + */ + public function __construct( + public array $includes = [], + public array $excludes = [], + public array $only = [], + public array $except = [], + public ?Wrap $wrap = null, + ) { + } +} diff --git a/src/Support/Transformation/LocalTransformationContext.php b/src/Support/Transformation/LocalTransformationContext.php new file mode 100644 index 00000000..8aeacb76 --- /dev/null +++ b/src/Support/Transformation/LocalTransformationContext.php @@ -0,0 +1,51 @@ +getDataContext(); + + $filter = fn (bool|null|Closure $condition, string $definition) => match (true) { + is_bool($condition) => $condition, + $condition === null => false, + is_callable($condition) => $condition($data), + }; + + return new self( + app(PartialsParser::class)->execute( + collect($dataContext->includes)->filter($filter)->keys()->all() + ), + app(PartialsParser::class)->execute( + collect($dataContext->excludes)->filter($filter)->keys()->all() + ), + app(PartialsParser::class)->execute( + collect($dataContext->only)->filter($filter)->keys()->all() + ), + app(PartialsParser::class)->execute( + collect($dataContext->except)->filter($filter)->keys()->all() + ), + ); + } +} diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php new file mode 100644 index 00000000..d684d256 --- /dev/null +++ b/src/Support/Transformation/TransformationContext.php @@ -0,0 +1,67 @@ +transformValues, + $this->mapPropertyNames, + $this->wrapExecutionType, + $this->lazyIncluded->getNested($property), + $this->lazyExcluded->getNested($property), + $this->only->getNested($property), + $this->except->getNested($property), + ); + } + + public function merge(LocalTransformationContext $localContext): self + { + return new self( + $this->transformValues, + $this->mapPropertyNames, + $this->wrapExecutionType, + $this->lazyIncluded->merge($localContext->lazyIncluded), + $this->lazyExcluded->merge($localContext->lazyExcluded), + $this->only->merge($localContext->only), + $this->except->merge($localContext->except), + ); + } + + public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self + { + return new self( + $this->transformValues, + $this->mapPropertyNames, + $wrapExecutionType, + $this->lazyIncluded, + $this->lazyExcluded, + $this->only, + $this->except, + ); + } +} diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php new file mode 100644 index 00000000..b00cd84b --- /dev/null +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -0,0 +1,139 @@ + $includes + * @param array $excludes + * @param array $only + * @param array $except + */ + protected function __construct( + public bool $transformValues = true, + public bool $mapPropertyNames = true, + public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, + public array $includes = [], + public array $excludes = [], + public array $only = [], + public array $except = [], + ) { + } + + public function get(): TransformationContext + { + return new TransformationContext( + $this->transformValues, + $this->mapPropertyNames, + $this->wrapExecutionType, + app(PartialsParser::class)->execute($this->includes), + app(PartialsParser::class)->execute($this->excludes), + app(PartialsParser::class)->execute($this->only), + app(PartialsParser::class)->execute($this->except), + ); + } + + public function transformValues(bool $transformValues = true): static + { + $this->transformValues = $transformValues; + + return $this; + } + + public function mapPropertyNames(bool $mapPropertyNames = true): static + { + $this->mapPropertyNames = $mapPropertyNames; + + return $this; + } + + public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static + { + $this->wrapExecutionType = $wrapExecutionType; + + return $this; + } + + public function include(string ...$includes): static + { + foreach ($includes as $include) { + $this->includes[$include] = true; + } + + return $this; + } + + public function exclude(string ...$excludes): static + { + foreach ($excludes as $exclude) { + $this->excludes[$exclude] = true; + } + + return $this; + } + + public function only(string ...$only): static + { + foreach ($only as $onlyDefinition) { + $this->only[$onlyDefinition] = true; + } + + return $this; + } + + public function except(string ...$except): static + { + foreach ($except as $exceptDefinition) { + $this->except[$exceptDefinition] = true; + } + + return $this; + } + + public function includeWhen(string $include, bool|Closure $condition): static + { + $this->includes[$include] = $condition; + + return $this; + } + + public function excludeWhen(string $exclude, bool|Closure $condition): static + { + $this->excludes[$exclude] = $condition; + + return $this; + } + + public function onlyWhen(string $only, bool|Closure $condition): static + { + $this->only[$only] = $condition; + + return $this; + } + + public function exceptWhen(string $except, bool|Closure $condition): static + { + $this->except[$except] = $condition; + + return $this; + } +} diff --git a/src/Support/Transforming/TransformationContext.php b/src/Support/Transforming/TransformationContext.php new file mode 100644 index 00000000..af946957 --- /dev/null +++ b/src/Support/Transforming/TransformationContext.php @@ -0,0 +1,24 @@ +getNested($name), ); - if ($value instanceof Optional) { - return $payload; - } - if ($this->mapPropertyNames && $property->outputMappedName) { $name = $property->outputMappedName; } diff --git a/tests/DataTest.php b/tests/DataTest.php index 03231e35..0196d548 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Inertia\LazyProp; use Spatie\LaravelData\Attributes\DataCollectionOf; @@ -22,6 +23,7 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Support\PartialTrees; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCast; @@ -85,9 +87,9 @@ }); it('can include a lazy property', function () { - $data = new LazyData(Lazy::create(fn () => 'test')); + $data = new LazyData(Lazy::create(fn() => 'test')); - expect($data->toArray())->toBe([]); +// expect($data->toArray())->toBe([]); expect($data->include('name')->toArray()) ->toMatchArray([ @@ -121,8 +123,8 @@ public function __construct( $data = new \TestIncludeableNestedLazyDataProperties( - Lazy::create(fn () => LazyData::from('Hello')), - Lazy::create(fn () => LazyData::collection(['is', 'it', 'me', 'your', 'looking', 'for',])), + Lazy::create(fn() => LazyData::from('Hello')), + Lazy::create(fn() => LazyData::collection(['is', 'it', 'me', 'your', 'looking', 'for',])), ); expect((clone $data)->toArray())->toBe([]); @@ -172,7 +174,7 @@ public function __construct( } } - $collection = Lazy::create(fn () => MultiLazyData::collection([ + $collection = Lazy::create(fn() => MultiLazyData::collection([ DummyDto::rick(), DummyDto::bon(), ])); @@ -228,7 +230,7 @@ public function __construct( public static function create(string $name): static { return new self( - Lazy::when(fn () => $name === 'Ruben', fn () => $name) + Lazy::when(fn() => $name === 'Ruben', fn() => $name) ); } }; @@ -252,7 +254,7 @@ public function __construct( public static function create(string $name): static { return new self( - Lazy::when(fn () => $name === 'Ruben', fn () => $name) + Lazy::when(fn() => $name === 'Ruben', fn() => $name) ); } }; @@ -314,7 +316,7 @@ public function __construct( public static function create(string $name): static { return new self( - Lazy::inertia(fn () => $name) + Lazy::inertia(fn() => $name) ); } }; @@ -484,7 +486,10 @@ public function __construct( 'only' => 'first_name', ])); - expect($response->getData(true))->toBe([]); + expect($response->getData(true))->toBe([ + 'first_name' => 'Ruben', + 'last_name' => 'Van Assche', + ]); OnlyData::$allowedOnly = ['first_name']; @@ -544,7 +549,7 @@ public function __construct( $data = new class ( $dataObject = new SimpleData('Test'), $dataCollection = SimpleData::collection([new SimpleData('A'), new SimpleData('B')]), - Lazy::create(fn () => new SimpleData('Lazy')), + Lazy::create(fn() => new SimpleData('Lazy')), 'Test', $transformable = new DateTime('16 may 1994') ) extends Data { @@ -569,7 +574,7 @@ public function __construct( expect($data->include('lazy')->all())->toMatchArray([ 'data' => $dataObject, 'dataCollection' => $dataCollection, - 'lazy' => (new SimpleData('Lazy'))->withPartialTrees(new PartialTrees(new ExcludedTreeNode())), + 'lazy' => (new SimpleData('Lazy')), 'string' => 'Test', 'transformable' => $transformable, ]); @@ -601,7 +606,8 @@ public function __construct( ) { } }; - expect($data)->transform(true, WrapExecutionType::Disabled, false) + + expect($data)->transform2(TransformationContextFactory::create()->mapPropertyNames(false)) ->toMatchArray([ 'camelName' => 'Freek', ]); @@ -616,7 +622,7 @@ public function __construct( } }; - expect($data)->transform(true, WrapExecutionType::Disabled, false) + expect($data)->transform2(TransformationContextFactory::create()->mapPropertyNames(false)) ->toMatchArray([ 'camelName' => 'Freek', ]); @@ -630,29 +636,11 @@ public function __construct( ) { } }; - expect($data->transform())->toMatchArray([ + expect($data->transform2())->toMatchArray([ 'snake_name' => 'Freek', ]); }); -it('can get the data object with mapping properties without transform data', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - - expect($data)->transform( - transformValues: false, - mapPropertyNames: true - ) - ->toMatchArray([ - 'snake_name' => 'Freek', - ]); -}); - it('can get the data object with mapping properties names', function () { $data = new class ('Freek', 'Hello World') extends Data { public function __construct( @@ -678,7 +666,7 @@ public function __construct(public string $name) $transformed = $data->additional([ 'company' => 'Spatie', - 'alt_name' => fn (Data $data) => "{$data->name} from Spatie", + 'alt_name' => fn(Data $data) => "{$data->name} from Spatie", ])->toArray(); expect($transformed)->toMatchArray([ @@ -960,7 +948,7 @@ public function __construct( public Data $nestedData, #[ WithTransformer(ConfidentialDataCollectionTransformer::class), - DataCollectionOf(SimpleData::class) + DataCollectionOf(SimpleData::class) ] public DataCollection $nestedDataCollection, ) { @@ -1154,7 +1142,7 @@ public function __construct( }); it('will not include lazy optional values when transforming', function () { - $data = new class ('Hello World', Lazy::create(fn () => Optional::make())) extends Data { + $data = new class ('Hello World', Lazy::create(fn() => Optional::make())) extends Data { public function __construct( public string $string, public string|Optional|Lazy $lazy_optional_string, @@ -1212,7 +1200,7 @@ public function __construct( public DataCollection $nested_collection, #[ MapOutputName('nested_other_collection'), - DataCollectionOf(SimpleDataWithMappedProperty::class) + DataCollectionOf(SimpleDataWithMappedProperty::class) ] public DataCollection $nested_renamed_collection, ) { @@ -1352,7 +1340,7 @@ public static function fromData(Data $data) expect( MultiLazyData::from(DummyDto::rick()) - ->includeWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') + ->includeWhen('name', fn(MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') ->toArray() ) ->toMatchArray([ @@ -1377,7 +1365,7 @@ public static function fromData(Data $data) it('can conditionally include using class defaults', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createLazy(enabled: false)) @@ -1391,7 +1379,7 @@ public static function fromData(Data $data) it('can conditionally include using class defaults nested', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createLazy(enabled: true)) @@ -1401,8 +1389,8 @@ public static function fromData(Data $data) it('can conditionally include using class defaults multiple', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createLazy(enabled: false)) @@ -1420,8 +1408,8 @@ public static function fromData(Data $data) it('can conditionally exclude', function () { $data = new MultiLazyData( - Lazy::create(fn () => 'Rick Astley')->defaultIncluded(), - Lazy::create(fn () => 'Never gonna give you up')->defaultIncluded(), + Lazy::create(fn() => 'Rick Astley')->defaultIncluded(), + Lazy::create(fn() => 'Never gonna give you up')->defaultIncluded(), 1989 ); @@ -1440,7 +1428,7 @@ public static function fromData(Data $data) expect( (clone $data) - ->exceptWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') + ->exceptWhen('name', fn(MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') ->toArray() ) ->toMatchArray([ @@ -1454,7 +1442,7 @@ public static function fromData(Data $data) public NestedLazyData $nested; }; - $data->nested = new NestedLazyData(Lazy::create(fn () => SimpleData::from('Hello World'))->defaultIncluded()); + $data->nested = new NestedLazyData(Lazy::create(fn() => SimpleData::from('Hello World'))->defaultIncluded()); expect($data->toArray())->toMatchArray([ 'nested' => ['simple' => ['string' => 'Hello World']], @@ -1466,7 +1454,7 @@ public static function fromData(Data $data) it('can conditionally exclude using class defaults', function () { PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) @@ -1487,7 +1475,7 @@ public static function fromData(Data $data) it('can conditionally exclude using class defaults nested', function () { PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) @@ -1508,8 +1496,8 @@ public static function fromData(Data $data) it('can conditionally exclude using multiple class defaults', function () { PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) @@ -1545,15 +1533,15 @@ public static function fromData(Data $data) expect( (clone $data) - ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') + ->onlyWhen('second', fn(MultiData $data) => $data->second === 'World') ->toArray() ) ->toMatchArray(['second' => 'World']); expect( (clone $data) - ->onlyWhen('first', fn (MultiData $data) => $data->first === 'Hello') - ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') + ->onlyWhen('first', fn(MultiData $data) => $data->first === 'Hello') + ->onlyWhen('second', fn(MultiData $data) => $data->second === 'World') ->toArray() ) ->toMatchArray([ @@ -1587,7 +1575,7 @@ public static function fromData(Data $data) it('can conditionally define only using class defaults', function () { PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1605,7 +1593,7 @@ public static function fromData(Data $data) it('can conditionally define only using class defaults nested', function () { PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1625,8 +1613,8 @@ public static function fromData(Data $data) it('can conditionally define only using multiple class defaults', function () { PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1661,7 +1649,7 @@ public static function fromData(Data $data) expect( (clone $data) - ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') + ->exceptWhen('second', fn(MultiData $data) => $data->second === 'World') ) ->toArray() ->toMatchArray([ @@ -1670,8 +1658,8 @@ public static function fromData(Data $data) expect( (clone $data) - ->exceptWhen('first', fn (MultiData $data) => $data->first === 'Hello') - ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') + ->exceptWhen('first', fn(MultiData $data) => $data->first === 'Hello') + ->exceptWhen('second', fn(MultiData $data) => $data->second === 'World') ->toArray() )->toBeEmpty(); }); @@ -1694,7 +1682,7 @@ public static function fromData(Data $data) it('can conditionally define except using class defaults', function () { PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1715,7 +1703,7 @@ public static function fromData(Data $data) it('can conditionally define except using class defaults nested', function () { PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1737,8 +1725,8 @@ public static function fromData(Data $data) it('can conditionally define except using multiple class defaults', function () { PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) diff --git a/tests/Datasets/DataTest.php b/tests/Datasets/DataTest.php index 8dea61a8..b77992b7 100644 --- a/tests/Datasets/DataTest.php +++ b/tests/Datasets/DataTest.php @@ -63,7 +63,7 @@ yield 'nested' => [ 'directive' => ['nested'], 'expectedOnly' => [ - 'nested' => [], + 'nested' => ['first' => 'C', 'second' => 'D'], ], 'expectedExcept' => [ 'first' => 'A', @@ -127,10 +127,8 @@ 'directive' => ['collection'], 'expectedOnly' => [ 'collection' => [ - [], - [], - // ['first' => 'E', 'second' => 'F'], - // ['first' => 'G', 'second' => 'H'], + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], ], ], 'expectedExcept' => [ diff --git a/tests/Normalizers/JsonNormalizerTest.php b/tests/Normalizers/JsonNormalizerTest.php index 97534cf6..862be7c7 100644 --- a/tests/Normalizers/JsonNormalizerTest.php +++ b/tests/Normalizers/JsonNormalizerTest.php @@ -8,7 +8,7 @@ $createdData = MultiData::from($originalData->toJson()); - expect($createdData)->toEqual($originalData); + expect($createdData->all())->toEqual($originalData->all()); }); it("won't create a data object from a regular string", function () { From c2ef22333c1b096c706bc97af46de6c408e29bf7 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 16 Feb 2023 11:01:37 +0100 Subject: [PATCH 003/124] wip --- src/Concerns/BaseDataCollectable.php | 2 +- src/Concerns/IncludeableData.php | 7 - src/Concerns/ResponsableData.php | 22 +- src/Contracts/IncludeableData.php | 4 - .../PartialsTreeFromRequestResolver.php | 17 +- .../TransformationContextFactory.php | 10 + .../DataCollectableTransformer.php | 85 ------ src/Transformers/DataTransformer.php | 254 ------------------ tests/DataCollectionTest.php | 1 - tests/DataTest.php | 1 - 10 files changed, 27 insertions(+), 376 deletions(-) delete mode 100644 src/Transformers/DataCollectableTransformer.php delete mode 100644 src/Transformers/DataTransformer.php diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 83747fb6..2b8ecb95 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -34,7 +34,7 @@ public function getDataClass(): string public function getIterator(): ArrayIterator { /** @var array $data */ - $data = $this->transform(transformValues: false); + $data = $this->transform2(TransformationContextFactory::create()->transformValues(false)); return new ArrayIterator($data); } diff --git a/src/Concerns/IncludeableData.php b/src/Concerns/IncludeableData.php index 228cbc26..2dc7894b 100644 --- a/src/Concerns/IncludeableData.php +++ b/src/Concerns/IncludeableData.php @@ -22,13 +22,6 @@ trait IncludeableData /** @var array */ protected array $_except = []; - public function withPartialTrees(PartialTrees $partialTrees): static - { - $this->_partialTrees = $partialTrees; - - return $this; - } - public function include(string ...$includes): static { foreach ($includes as $include) { diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 873bfbd3..eb0bc457 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -22,20 +22,14 @@ trait ResponsableData */ public function toResponse($request) { - $context = new TransformationContext( - wrapExecutionType: WrapExecutionType::Enabled, - ); - - if ($this instanceof IncludeableDataContract) { - $partialTrees = resolve(PartialsTreeFromRequestResolver::class)->execute($this, $request); - - $context = $context->merge(new LocalTransformationContext( - $partialTrees->lazyIncluded, - $partialTrees->lazyExcluded, - $partialTrees->only, - $partialTrees->except, - )); - } + $context = TransformationContextFactory::create() + ->wrapExecutionType(WrapExecutionType::Enabled) + ->mergeDataContext($this->getDataContext()) + ->get(); + + $context = $this instanceof IncludeableDataContract + ? $context->merge(resolve(PartialsTreeFromRequestResolver::class)->execute($this, $request)) + : $context; return new JsonResponse( data: $this->transform2($context), diff --git a/src/Contracts/IncludeableData.php b/src/Contracts/IncludeableData.php index e4c168d5..bc665a28 100644 --- a/src/Contracts/IncludeableData.php +++ b/src/Contracts/IncludeableData.php @@ -7,8 +7,6 @@ interface IncludeableData { - public function withPartialTrees(PartialTrees $partialTrees): object; - public function include(string ...$includes): object; public function exclude(string ...$excludes): object; @@ -24,6 +22,4 @@ public function excludeWhen(string $exclude, bool|Closure $condition): object; public function onlyWhen(string $only, bool|Closure $condition): object; public function exceptWhen(string $except, bool|Closure $condition): object; - - public function getPartialTrees(): PartialTrees; } diff --git a/src/Resolvers/PartialsTreeFromRequestResolver.php b/src/Resolvers/PartialsTreeFromRequestResolver.php index 39130585..5e15ea81 100644 --- a/src/Resolvers/PartialsTreeFromRequestResolver.php +++ b/src/Resolvers/PartialsTreeFromRequestResolver.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\PartialsParser; use Spatie\LaravelData\Support\PartialTrees; +use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; use TypeError; class PartialsTreeFromRequestResolver @@ -22,9 +23,9 @@ public function __construct( } public function execute( - IncludeableData $data, + BaseData|BaseDataCollectable $data, Request $request, - ): PartialTrees { + ): LocalTransformationContext { $requestedIncludesTree = $this->partialsParser->execute( $request->has('include') ? $this->arrayFromRequest($request, 'include') : [] ); @@ -49,13 +50,11 @@ public function execute( $allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $this->dataConfig->getDataClass($dataClass)); $allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $this->dataConfig->getDataClass($dataClass)); - $partialTrees = $data->getPartialTrees(); - - return new PartialTrees( - $partialTrees->lazyIncluded->merge($requestedIncludesTree->intersect($allowedRequestIncludesTree)), - $partialTrees->lazyExcluded->merge($requestedExcludesTree->intersect($allowedRequestExcludesTree)), - $partialTrees->only->merge($requestedOnlyTree->intersect($allowedRequestOnlyTree)), - $partialTrees->except->merge($requestedExceptTree->intersect($allowedRequestExceptTree)) + return new LocalTransformationContext( + $requestedIncludesTree->intersect($allowedRequestIncludesTree), + $requestedExcludesTree->intersect($allowedRequestExcludesTree), + $requestedOnlyTree->intersect($allowedRequestOnlyTree), + $requestedExceptTree->intersect($allowedRequestExceptTree) ); } diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index b00cd84b..e9b227f6 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -136,4 +136,14 @@ public function exceptWhen(string $except, bool|Closure $condition): static return $this; } + + public function mergeDataContext(DataContext $context): static + { + $this->includes = array_merge($this->includes, $context->includes); + $this->excludes = array_merge($this->excludes, $context->excludes); + $this->only = array_merge($this->only, $context->only); + $this->except = array_merge($this->includes, $context->except); + + return $this; + } } diff --git a/src/Transformers/DataCollectableTransformer.php b/src/Transformers/DataCollectableTransformer.php deleted file mode 100644 index 7a27b3c5..00000000 --- a/src/Transformers/DataCollectableTransformer.php +++ /dev/null @@ -1,85 +0,0 @@ -items instanceof Enumerable) { - return $this->transformCollection($this->items); - } - - $this->items->through( - $this->transformItemClosure() - ); - - return $this->transformValues - ? $this->wrapPaginatedArray($this->items->toArray()) - : $this->items->all(); - } - - protected function transformCollection(Enumerable $items): array - { - $items = $items->map($this->transformItemClosure()) - ->when( - $this->transformValues, - fn (Enumerable $collection) => $collection->map(fn (TransformableData $data) => $data->transform( - $this->transformValues, - $this->wrapExecutionType->shouldExecute() - ? WrapExecutionType::TemporarilyDisabled - : $this->wrapExecutionType, - $this->mapPropertyNames, - )) - ) - ->all(); - - return $this->wrapExecutionType->shouldExecute() - ? $this->wrap->wrap($items) - : $items; - } - - protected function transformItemClosure(): Closure - { - return fn (BaseData $item) => $item instanceof IncludeableData - ? $item->withPartialTrees($this->trees) - : $item; - } - - protected function wrapPaginatedArray(array $paginated): array - { - $wrapKey = $this->wrap->getKey() ?? 'data'; - - return [ - $wrapKey => $paginated['data'], - 'links' => $paginated['links'] ?? [], - 'meta' => Arr::except($paginated, [ - 'data', - 'links', - ]), - ]; - } -} diff --git a/src/Transformers/DataTransformer.php b/src/Transformers/DataTransformer.php deleted file mode 100644 index 4262e203..00000000 --- a/src/Transformers/DataTransformer.php +++ /dev/null @@ -1,254 +0,0 @@ -config = app(DataConfig::class); - } - - /** @return array */ - public function transform(TransformableData $data): array - { - $transformed = $this->resolvePayload($data); - - if ($data instanceof WrappableData && $this->wrapExecutionType->shouldExecute()) { - $transformed = $data->getWrap()->wrap($transformed); - } - - if ($data instanceof AppendableData) { - $transformed = array_merge($transformed, $data->getAdditionalData()); - } - - return $transformed; - } - - /** @return array */ - protected function resolvePayload(TransformableData $data): array - { - $trees = $data instanceof IncludeableData - ? $data->getPartialTrees() - : new PartialTrees(); - - $dataClass = $this->config->getDataClass($data::class); - - return $dataClass - ->properties - ->reduce(function (array $payload, DataProperty $property) use ($data, $trees) { - $name = $property->name; - - if (! $this->shouldIncludeProperty($name, $data->{$name}, $trees)) { - return $payload; - } - - $value = $this->resolvePropertyValue( - $property, - $data->{$name}, - $trees->getNested($name), - ); - - if ($this->mapPropertyNames && $property->outputMappedName) { - $name = $property->outputMappedName; - } - - $payload[$name] = $value; - - return $payload; - }, []); - } - - protected function shouldIncludeProperty( - string $name, - mixed $value, - PartialTrees $trees, - ): bool { - if ($value instanceof Optional) { - return false; - } - - if ($this->isPropertyHidden($name, $trees)) { - return false; - } - - if (! $value instanceof Lazy) { - return true; - } - - if ($value instanceof RelationalLazy || $value instanceof ConditionalLazy) { - return $value->shouldBeIncluded(); - } - - if ($this->isPropertyLazyExcluded($name, $trees)) { - return false; - } - - return $this->isPropertyLazyIncluded($name, $value, $trees); - } - - protected function isPropertyHidden( - string $name, - PartialTrees $trees, - ): bool { - if ($trees->except instanceof AllTreeNode) { - return true; - } - - if ( - $trees->except instanceof PartialTreeNode - && $trees->except->hasField($name) - && $trees->except->getNested($name) instanceof ExcludedTreeNode - ) { - return true; - } - - if ($trees->except instanceof PartialTreeNode) { - return false; - } - - if ($trees->only instanceof AllTreeNode) { - return false; - } - - if ($trees->only instanceof PartialTreeNode && $trees->only->hasField($name)) { - return false; - } - - if ($trees->only instanceof PartialTreeNode || $trees->only instanceof ExcludedTreeNode) { - return true; - } - - return false; - } - - protected function isPropertyLazyExcluded( - string $name, - PartialTrees $trees, - ): bool { - if ($trees->lazyExcluded instanceof AllTreeNode) { - return true; - } - - return $trees->lazyExcluded instanceof PartialTreeNode && $trees->lazyExcluded->hasField($name); - } - - protected function isPropertyLazyIncluded( - string $name, - Lazy $value, - PartialTrees $trees, - ): bool { - if ($trees->lazyIncluded instanceof AllTreeNode) { - return true; - } - - if ($value->isDefaultIncluded()) { - return true; - } - - return $trees->lazyIncluded instanceof PartialTreeNode && $trees->lazyIncluded->hasField($name); - } - - protected function resolvePropertyValue( - DataProperty $property, - mixed $value, - PartialTrees $trees - ): mixed { - if ($value instanceof Lazy) { - $value = $value->resolve(); - } - - if ($value === null) { - return null; - } - - 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)) { - $value = Arr::except($value, $trees->except->getFields()); - } - - if ($transformer = $this->resolveTransformerForValue($property, $value)) { - return $transformer->transform($property, $value); - } - - if (! $value instanceof BaseData && ! $value instanceof BaseDataCollectable) { - return $value; - } - - if ($value instanceof IncludeableData) { - $value->withPartialTrees($trees); - } - - $wrapExecutionType = match (true) { - $value instanceof BaseData && $this->wrapExecutionType === WrapExecutionType::Enabled => WrapExecutionType::TemporarilyDisabled, - $value instanceof BaseData && $this->wrapExecutionType === WrapExecutionType::Disabled => WrapExecutionType::Disabled, - $value instanceof BaseData && $this->wrapExecutionType === WrapExecutionType::TemporarilyDisabled => WrapExecutionType::TemporarilyDisabled, - $value instanceof BaseDataCollectable && $this->wrapExecutionType === WrapExecutionType::Enabled => WrapExecutionType::Enabled, - $value instanceof BaseDataCollectable && $this->wrapExecutionType === WrapExecutionType::Disabled => WrapExecutionType::Disabled, - $value instanceof BaseDataCollectable && $this->wrapExecutionType === WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled, - default => throw new TypeError('Invalid wrap execution type') - }; - - if ($value instanceof TransformableData && $this->transformValues) { - return $value->transform($this->transformValues, $wrapExecutionType, $this->mapPropertyNames, ); - } - - return $value; - } - - protected function resolveTransformerForValue( - DataProperty $property, - mixed $value, - ): ?Transformer { - if (! $this->transformValues) { - return null; - } - - $transformer = $property->transformer ?? $this->config->findGlobalTransformerForValue($value); - - $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer - && ($property->type->isDataObject || $property->type->isDataCollectable); - - if ($shouldUseDefaultDataTransformer) { - return null; - } - - return $transformer; - } -} diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 06a4d18c..2e95a553 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -497,7 +497,6 @@ function (string $operation, array $arguments, array $expected) { it('during the serialization process some properties are thrown away', function () { $collection = SimpleData::collection(['A', 'B']); - $collection->withPartialTrees(new PartialTrees()); $collection->include('test'); $collection->exclude('test'); $collection->only('test'); diff --git a/tests/DataTest.php b/tests/DataTest.php index 0196d548..9bb76734 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -2191,7 +2191,6 @@ public function __construct( it('during the serialization process some properties are thrown away', function () { $object = SimpleData::from('Hello world'); - $object->withPartialTrees(new PartialTrees()); $object->include('test'); $object->exclude('test'); $object->only('test'); From e6d7ab73f9fc9e6e1488c98a1d4a4899766f5a80 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 21 Feb 2023 15:20:03 +0100 Subject: [PATCH 004/124] wip --- src/Concerns/BaseData.php | 69 ++-- src/Concerns/BaseDataCollectable.php | 53 +-- src/Concerns/DataTrait.php | 2 +- src/Concerns/EmptyData.php | 42 ++ src/Concerns/IncludeableData.php | 112 +---- src/Concerns/PrepareableData.php | 13 - src/Concerns/ResponsableData.php | 13 +- src/Concerns/TransformableData.php | 42 +- src/Contracts/BaseData.php | 6 +- src/Contracts/DataObject.php | 2 +- src/Contracts/EmptyData.php | 19 + src/Contracts/PrepareableData.php | 8 - src/Contracts/TransformableData.php | 12 +- src/DataPipes/CastPropertiesDataPipe.php | 10 +- src/Dto.php | 14 + src/Enums/CustomCreationMethodType.php | 10 + src/Enums/DataCollectableType.php | 2 + src/Enums/DataTypeKind.php | 34 ++ .../CannotCreateDataCollectable.php | 15 + .../DataCollectableFromSomethingResolver.php | 121 ++++++ src/Resolvers/DataFromSomethingResolver.php | 3 +- ...alidationMessagesAndAttributesResolver.php | 8 +- src/Resolvers/DataValidationRulesResolver.php | 7 +- src/Resolvers/EmptyDataResolver.php | 4 +- .../PartialsTreeFromRequestResolver.php | 6 +- src/Resolvers/TransformedDataResolver.php | 48 +-- src/Resource.php | 29 ++ src/RuleInferrers/RequiredRuleInferrer.php | 2 +- src/Support/AllowedPartialsParser.php | 3 +- .../Annotations/DataCollectableAnnotation.php | 14 + .../DataCollectableAnnotationReader.php | 214 ++++++++++ src/Support/DataClass.php | 31 +- src/Support/DataMethod.php | 52 ++- src/Support/DataParameter.php | 3 +- src/Support/DataProperty.php | 6 +- src/Support/DataType.php | 263 +----------- src/Support/Factories/DataTypeFactory.php | 281 +++++++++++++ src/Support/PartialTrees.php | 40 -- .../Partials/ForwardsToPartialsDefinition.php | 74 ++++ src/Support/Partials/PartialsDefinition.php | 23 ++ src/Support/Transformation/DataContext.php | 13 +- .../LocalTransformationContext.php | 51 --- .../PartialTransformationContext.php | 71 ++++ .../Transformation/TransformationContext.php | 22 +- .../TransformationContextFactory.php | 97 +---- src/Support/Type.php | 165 ++++++++ .../DataTypeScriptTransformer.php | 13 +- .../References/RouteParameterReference.php | 9 +- tests/DataCollectionTest.php | 6 +- tests/DataTest.php | 45 ++- tests/Fakes/CollectionAnnotationsData.php | 67 +-- tests/Resolvers/EmptyDataResolverTest.php | 6 +- .../DataCollectableAnnotationReaderTest.php | 100 +++++ tests/Support/DataClassTest.php | 68 ++-- tests/Support/DataMethodTest.php | 115 +++++- tests/Support/DataParameterTest.php | 9 +- tests/Support/DataTypeTest.php | 381 +++++++++++------- 57 files changed, 1977 insertions(+), 971 deletions(-) create mode 100644 src/Concerns/EmptyData.php delete mode 100644 src/Concerns/PrepareableData.php create mode 100644 src/Contracts/EmptyData.php delete mode 100644 src/Contracts/PrepareableData.php create mode 100644 src/Dto.php create mode 100644 src/Enums/CustomCreationMethodType.php create mode 100644 src/Enums/DataTypeKind.php create mode 100644 src/Exceptions/CannotCreateDataCollectable.php create mode 100644 src/Resolvers/DataCollectableFromSomethingResolver.php create mode 100644 src/Resource.php create mode 100644 src/Support/Annotations/DataCollectableAnnotation.php create mode 100644 src/Support/Annotations/DataCollectableAnnotationReader.php create mode 100644 src/Support/Factories/DataTypeFactory.php delete mode 100644 src/Support/PartialTrees.php create mode 100644 src/Support/Partials/ForwardsToPartialsDefinition.php create mode 100644 src/Support/Partials/PartialsDefinition.php delete mode 100644 src/Support/Transformation/LocalTransformationContext.php create mode 100644 src/Support/Transformation/PartialTransformationContext.php create mode 100644 src/Support/Type.php create mode 100644 tests/Support/Annotations/DataCollectableAnnotationReaderTest.php diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 3b1e98a5..3356c5fe 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; @@ -19,19 +20,20 @@ use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Resolvers\EmptyDataResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Transformation\DataContext; -use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; +use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Support\Wrapping\WrapType; -use Spatie\LaravelData\Transformers\DataTransformer; trait BaseData { @@ -74,6 +76,24 @@ public static function withoutMagicalCreationFrom(mixed ...$payloads): static ); } + public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator + { + return app(DataCollectableFromSomethingResolver::class)->execute( + static::class, + $items, + $into + ); + } + + public static function withoutMagicalCreationCollect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator + { + return app(DataCollectableFromSomethingResolver::class)->withoutMagicalCreation()->execute( + static::class, + $items, + $into + ); + } + public static function normalizers(): array { return config('data.normalizers'); @@ -91,6 +111,11 @@ public static function pipeline(): DataPipeline ->through(CastPropertiesDataPipe::class); } + public static function prepareForPipeline(Collection $properties): Collection + { + return $properties; + } + public static function collection(Enumerable|array|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator|DataCollection $items): DataCollection|CursorPaginatedDataCollection|PaginatedDataCollection { if ($items instanceof Paginator || $items instanceof AbstractPaginator) { @@ -104,36 +129,6 @@ public static function collection(Enumerable|array|AbstractPaginator|Paginator|A return new (static::$_collectionClass)(static::class, $items); } - public static function empty(array $extra = []): array - { - return app(EmptyDataResolver::class)->execute(static::class, $extra); - } - - public function transform( - bool $transformValues = true, - WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - bool $mapPropertyNames = true, - ): array { - return DataTransformer::create($transformValues, $wrapExecutionType, $mapPropertyNames)->transform($this); - } - - public function transform2( - null|TransformationContextFactory|TransformationContext $context = null, - ): array { - if ($context === null) { - $context = new TransformationContext(); - } - - if ($context instanceof TransformationContextFactory) { - $context = $context->get(); - } - - return app(TransformedDataResolver::class)->execute( - $this, - $context->merge(LocalTransformationContext::create($this)) - ); - } - public function __sleep(): array { return app(DataConfig::class)->getDataClass(static::class) @@ -147,10 +142,12 @@ public function getDataContext(): DataContext { if ($this->_dataContext === null) { return $this->_dataContext = new DataContext( - $this instanceof IncludeableDataContract ? $this->includeProperties() : [], - $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], - $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], - $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + new PartialsDefinition( + $this instanceof IncludeableDataContract ? $this->includeProperties() : [], + $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], + $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], + $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + ), $this instanceof WrappableDataContract ? $this->getWrap() : new Wrap(WrapType::UseGlobal), ); } diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 2b8ecb95..2d2384e6 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -7,8 +7,9 @@ use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Transformation\DataContext; -use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; +use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Support\Wrapping\Wrap; @@ -34,7 +35,7 @@ public function getDataClass(): string public function getIterator(): ArrayIterator { /** @var array $data */ - $data = $this->transform2(TransformationContextFactory::create()->transformValues(false)); + $data = $this->transform(TransformationContextFactory::create()->transformValues(false)); return new ArrayIterator($data); } @@ -44,44 +45,6 @@ public function count(): int return $this->items->count(); } - /** - * @return array - */ - public function transform( - bool $transformValues = true, - WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - bool $mapPropertyNames = true, - ): array { - $transformer = new DataCollectableTransformer( - $this->dataClass, - $transformValues, - $wrapExecutionType, - $mapPropertyNames, - $this->getPartialTrees(), - $this->items, - $this->getWrap(), - ); - - return $transformer->transform(); - } - - public function transform2( - null|TransformationContextFactory|TransformationContext $context = null, - ): array { - if ($context === null) { - $context = new TransformationContext(); - } - - if ($context instanceof TransformationContextFactory) { - $context = $context->get(); - } - - return app(TransformedDataCollectionResolver::class)->execute( - $this, - $context->merge(LocalTransformationContext::create($this)) - ); - } - public function __sleep(): array { return ['items', 'dataClass']; @@ -91,10 +54,12 @@ public function getDataContext(): DataContext { if ($this->_dataContext === null) { return $this->_dataContext = new DataContext( - $this instanceof IncludeableDataContract ? $this->includeProperties() : [], - $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], - $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], - $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + new PartialsDefinition( + $this instanceof IncludeableDataContract ? $this->includeProperties() : [], + $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], + $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], + $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + ), $this instanceof WrappableDataContract ? $this->getWrap() : new Wrap(WrapType::UseGlobal), ); } diff --git a/src/Concerns/DataTrait.php b/src/Concerns/DataTrait.php index 4fcfb4d2..fdfd08a8 100644 --- a/src/Concerns/DataTrait.php +++ b/src/Concerns/DataTrait.php @@ -7,9 +7,9 @@ trait DataTrait use ResponsableData; use IncludeableData; use AppendableData; - use PrepareableData; use ValidateableData; use WrappableData; use TransformableData; use BaseData; + use EmptyData; } diff --git a/src/Concerns/EmptyData.php b/src/Concerns/EmptyData.php new file mode 100644 index 00000000..c2f006ef --- /dev/null +++ b/src/Concerns/EmptyData.php @@ -0,0 +1,42 @@ +execute(static::class, $extra); + } +} diff --git a/src/Concerns/IncludeableData.php b/src/Concerns/IncludeableData.php index 2dc7894b..3b3b8738 100644 --- a/src/Concerns/IncludeableData.php +++ b/src/Concerns/IncludeableData.php @@ -3,95 +3,18 @@ namespace Spatie\LaravelData\Concerns; use Closure; +use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; +use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\PartialsParser; use Spatie\LaravelData\Support\PartialTrees; trait IncludeableData { - protected ?PartialTrees $_partialTrees = null; + use ForwardsToPartialsDefinition; - /** @var array */ - protected array $_includes = []; - - /** @var array */ - protected array $_excludes = []; - - /** @var array */ - protected array $_only = []; - - /** @var array */ - protected array $_except = []; - - public function include(string ...$includes): static - { - foreach ($includes as $include) { - $this->getDataContext()->includes[$include] = true; - $this->_includes[$include] = true; - } - - return $this; - } - - public function exclude(string ...$excludes): static - { - foreach ($excludes as $exclude) { - $this->getDataContext()->excludes[$exclude] = true; - $this->_excludes[$exclude] = true; - } - - return $this; - } - - public function only(string ...$only): static - { - foreach ($only as $onlyDefinition) { - $this->getDataContext()->only[$onlyDefinition] = true; - $this->_only[$onlyDefinition] = true; - } - - return $this; - } - - public function except(string ...$except): static + protected function getPartialsDefinition(): PartialsDefinition { - foreach ($except as $exceptDefinition) { - $this->getDataContext()->except[$exceptDefinition] = true; - $this->_except[$exceptDefinition] = true; - } - - return $this; - } - - public function includeWhen(string $include, bool|Closure $condition): static - { - $this->getDataContext()->includes[$include] = $condition; - $this->_includes[$include] = $condition; - - return $this; - } - - public function excludeWhen(string $exclude, bool|Closure $condition): static - { - $this->getDataContext()->excludes[$exclude] = $condition; - $this->_excludes[$exclude] = $condition; - - return $this; - } - - public function onlyWhen(string $only, bool|Closure $condition): static - { - $this->getDataContext()->only[$only] = $condition; - $this->_only[$only] = $condition; - - return $this; - } - - public function exceptWhen(string $except, bool|Closure $condition): static - { - $this->getDataContext()->except[$except] = $condition; - $this->_except[$except] = $condition; - - return $this; + return $this->getDataContext()->partialsDefinition; } protected function includeProperties(): array @@ -113,29 +36,4 @@ protected function exceptProperties(): array { return []; } - - public function getPartialTrees(): PartialTrees - { - if ($this->_partialTrees) { - return $this->_partialTrees; - } - - $filter = fn (bool|null|Closure $condition, string $definition) => match (true) { - is_bool($condition) => $condition, - $condition === null => false, - is_callable($condition) => $condition($this), - }; - - $includes = collect($this->_includes)->merge($this->includeProperties())->filter($filter)->keys()->all(); - $excludes = collect($this->_excludes)->merge($this->excludeProperties())->filter($filter)->keys()->all(); - $only = collect($this->_only)->merge($this->onlyProperties())->filter($filter)->keys()->all(); - $except = collect($this->_except)->merge($this->exceptProperties())->filter($filter)->keys()->all(); - - return new PartialTrees( - (new PartialsParser())->execute($includes), - (new PartialsParser())->execute($excludes), - (new PartialsParser())->execute($only), - (new PartialsParser())->execute($except), - ); - } } diff --git a/src/Concerns/PrepareableData.php b/src/Concerns/PrepareableData.php deleted file mode 100644 index d4b039ab..00000000 --- a/src/Concerns/PrepareableData.php +++ /dev/null @@ -1,13 +0,0 @@ -wrapExecutionType(WrapExecutionType::Enabled) - ->mergeDataContext($this->getDataContext()) - ->get(); + ->get($this) + ->mergePartials(PartialTransformationContext::create( + $this, + $this->getDataContext()->partialsDefinition) + ); $context = $this instanceof IncludeableDataContract - ? $context->merge(resolve(PartialsTreeFromRequestResolver::class)->execute($this, $request)) + ? $context->mergePartials(resolve(PartialsTreeFromRequestResolver::class)->execute($this, $request)) : $context; return new JsonResponse( - data: $this->transform2($context), + data: $this->transform($context), status: $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK, ); } diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 8fb48137..7ed98313 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -2,30 +2,64 @@ namespace Spatie\LaravelData\Concerns; +use Exception; +use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; +use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; +use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; +use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast; +use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; trait TransformableData { + public function transform( + null|TransformationContextFactory|TransformationContext $context = null, + ): array { + if ($context === null) { + $context = new TransformationContext(); + } + + if ($context instanceof TransformationContextFactory) { + $context = $context->get($this); + } + + $resolver = match (true) { + $this instanceof BaseDataContract => app(TransformedDataResolver::class), + $this instanceof BaseDataCollectableContract => app(TransformedDataCollectionResolver::class), + default => throw new Exception('Cannot transform data object') + }; + + $localPartials = PartialTransformationContext::create( + $this, + $this->getDataContext()->partialsDefinition + ); + + return $resolver->execute( + $this, + $context->mergePartials($localPartials) + ); + } + public function all(): array { - return $this->transform2(TransformationContextFactory::create()->transformValues(false)); + return $this->transform(TransformationContextFactory::create()->transformValues(false)); } public function toArray(): array { - return $this->transform2(); + return $this->transform(); } public function toJson($options = 0): string { - return json_encode($this->transform2(), $options); + return json_encode($this->transform(), $options); } public function jsonSerialize(): array { - return $this->transform2(); + return $this->transform(); } public static function castUsing(array $arguments) diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 003fb0af..d8878d5a 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -24,6 +24,10 @@ public static function from(mixed ...$payloads): static; public static function withoutMagicalCreationFrom(mixed ...$payloads): static; + public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator; + + public static function withoutMagicalCreationCollect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator; + /** * @param \Illuminate\Support\Enumerable|TValue[]|\Illuminate\Pagination\AbstractPaginator|\Illuminate\Contracts\Pagination\Paginator|\Illuminate\Pagination\AbstractCursorPaginator|\Illuminate\Contracts\Pagination\CursorPaginator|\Spatie\LaravelData\DataCollection $items * @@ -35,7 +39,7 @@ public static function normalizers(): array; public static function pipeline(): DataPipeline; - public static function empty(array $extra = []): array; + public static function prepareForPipeline(\Illuminate\Support\Collection $properties): \Illuminate\Support\Collection; public function getDataContext(): DataContext; } diff --git a/src/Contracts/DataObject.php b/src/Contracts/DataObject.php index e7e2fd5f..7dfe9916 100644 --- a/src/Contracts/DataObject.php +++ b/src/Contracts/DataObject.php @@ -4,6 +4,6 @@ use Illuminate\Contracts\Support\Responsable; -interface DataObject extends Responsable, AppendableData, BaseData, TransformableData, IncludeableData, ResponsableData, ValidateableData, WrappableData, PrepareableData +interface DataObject extends Responsable, AppendableData, BaseData, TransformableData, IncludeableData, ResponsableData, ValidateableData, WrappableData, EmptyData { } diff --git a/src/Contracts/EmptyData.php b/src/Contracts/EmptyData.php new file mode 100644 index 00000000..3ec4f798 --- /dev/null +++ b/src/Contracts/EmptyData.php @@ -0,0 +1,19 @@ +all(); foreach ($properties as $name => $value) { - $dataProperty = $class->properties->first(fn (DataProperty $dataProperty) => $dataProperty->name === $name); + $dataProperty = $class->properties->first(fn(DataProperty $dataProperty) => $dataProperty->name === $name); if ($dataProperty === null) { continue; @@ -56,12 +58,14 @@ protected function cast( return $cast->cast($property, $value, $castContext); } - if ($property->type->isDataObject) { + if ($property->type->kind->isDataObject()) { return $property->type->dataClass::from($value); } - if ($property->type->isDataCollectable) { + if ($property->type->kind->isDataCollectable()) { return $property->type->dataClass::collection($value); + // TODO: future + // return $property->type->dataClass::collect($value, $property->type->dataCollectableClass); } return $value; diff --git a/src/Dto.php b/src/Dto.php new file mode 100644 index 00000000..3e566fc5 --- /dev/null +++ b/src/Dto.php @@ -0,0 +1,14 @@ +withoutMagicalCreation = $withoutMagicalCreation; + + return $this; + } + + public function ignoreMagicalMethods(string ...$methods): self + { + array_push($this->ignoredMagicalMethods, ...$methods); + + return $this; + } + + public function execute( + string $dataClass, + mixed $items, + ?string $into = null, + ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { + $from = match (gettype($items)) { + 'object' => $items::class, + 'array' => 'array', + default => throw new Exception('Unknown type provided to create a collectable') + }; + + $into ??= $from; + + $collectable = $this->createFromCustomCreationMethod($dataClass, $items, $into); + + if ($collectable) { + return $collectable; + } + + if ($into === 'array') { + return $this->createArray($dataClass, $from, $items); + } + } + + protected function createFromCustomCreationMethod( + string $dataClass, + mixed $items, + string $into, + ): null|array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { + if ($this->withoutMagicalCreation) { + return null; + } + + /** @var ?DataMethod $method */ + $method = $this->dataConfig + ->getDataClass($dataClass) + ->methods + ->filter( + fn(DataMethod $method) => $method->customCreationMethodType === CustomCreationMethodType::Collection + && ! in_array($method->name, $this->ignoredMagicalMethods) + && $method->returns($into) + && $method->accepts([$items]) + ) + ->first(); + + if ($method !== null) { + return $dataClass::{$method->name}($items); + } + + return null; + } + + protected function createArray( + string $dataClass, + string $from, + mixed $items + ): array { + if ($items instanceof DataCollection) { + $items = $items->items(); + } + + if ($items instanceof Enumerable) { + $items = $items->all(); + } + + if (is_array($items)) { + return array_map( + fn(mixed $data) => $dataClass::from($data), + $items, + ); + } + + throw CannotCreateDataCollectable::create($from, 'array'); + } +} diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index ce75e5cb..2827f1d6 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Enums\CustomCreationMethodType; use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; @@ -68,7 +69,7 @@ protected function createFromCustomCreationMethod(string $class, array $payloads ->getDataClass($class) ->methods ->filter( - fn (DataMethod $method) => $method->isCustomCreationMethod + fn (DataMethod $method) => $method->customCreationMethodType === CustomCreationMethodType::Object && ! in_array($method->name, $this->ignoredMagicalMethods) ); diff --git a/src/Resolvers/DataValidationMessagesAndAttributesResolver.php b/src/Resolvers/DataValidationMessagesAndAttributesResolver.php index 94ee637b..7b279bcc 100644 --- a/src/Resolvers/DataValidationMessagesAndAttributesResolver.php +++ b/src/Resolvers/DataValidationMessagesAndAttributesResolver.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Resolvers; use Illuminate\Support\Arr; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Validation\ValidationPath; @@ -27,8 +28,7 @@ public function execute( $propertyPath = $path->property($dataProperty->inputMappedName ?? $dataProperty->name); if ( - $dataProperty->type->isDataObject === false - && $dataProperty->type->isDataCollectable === false + $dataProperty->type->kind === DataTypeKind::Default && $dataProperty->validate === false ) { continue; @@ -38,7 +38,7 @@ public function execute( continue; } - if ($dataProperty->type->isDataObject) { + if ($dataProperty->type->kind->isDataObject()) { $nested = $this->execute( $dataProperty->type->dataClass, $fullPayload, @@ -51,7 +51,7 @@ public function execute( continue; } - if ($dataProperty->type->isDataCollectable) { + if ($dataProperty->type->kind->isDataCollectable()) { $collected = $this->execute( $dataProperty->type->dataClass, $fullPayload, diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index 4da6fc38..003edc3f 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -7,6 +7,7 @@ use Illuminate\Validation\Rule; use Spatie\LaravelData\Attributes\Validation\ArrayType; use Spatie\LaravelData\Attributes\Validation\Present; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; @@ -41,7 +42,7 @@ public function execute( continue; } - if ($dataProperty->type->isDataObject || $dataProperty->type->isDataCollectable) { + if ($dataProperty->type->kind !== DataTypeKind::Default) { $this->resolveDataSpecificRules( $dataProperty, $fullPayload, @@ -83,7 +84,7 @@ protected function resolveDataSpecificRules( return; } - if ($dataProperty->type->isDataObject) { + if ($dataProperty->type->kind->isDataObject()) { $this->resolveDataObjectSpecificRules( $dataProperty, $fullPayload, @@ -95,7 +96,7 @@ protected function resolveDataSpecificRules( return; } - if ($dataProperty->type->isDataCollectable) { + if ($dataProperty->type->kind->isDataCollectable()) { $this->resolveDataCollectionSpecificRules( $dataProperty, $fullPayload, diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index b67ed410..d1498433 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -44,11 +44,11 @@ protected function getValueForProperty(DataProperty $property): mixed return []; } - if ($property->type->isDataObject) { + if ($property->type->kind->isDataObject()) { return $property->type->dataClass::empty(); } - if ($property->type->isDataCollectable) { + if ($property->type->kind->isDataCollectable()) { return []; } diff --git a/src/Resolvers/PartialsTreeFromRequestResolver.php b/src/Resolvers/PartialsTreeFromRequestResolver.php index 5e15ea81..f4ae0394 100644 --- a/src/Resolvers/PartialsTreeFromRequestResolver.php +++ b/src/Resolvers/PartialsTreeFromRequestResolver.php @@ -10,7 +10,7 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\PartialsParser; use Spatie\LaravelData\Support\PartialTrees; -use Spatie\LaravelData\Support\Transformation\LocalTransformationContext; +use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use TypeError; class PartialsTreeFromRequestResolver @@ -25,7 +25,7 @@ public function __construct( public function execute( BaseData|BaseDataCollectable $data, Request $request, - ): LocalTransformationContext { + ): PartialTransformationContext { $requestedIncludesTree = $this->partialsParser->execute( $request->has('include') ? $this->arrayFromRequest($request, 'include') : [] ); @@ -50,7 +50,7 @@ public function execute( $allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $this->dataConfig->getDataClass($dataClass)); $allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $this->dataConfig->getDataClass($dataClass)); - return new LocalTransformationContext( + return new PartialTransformationContext( $requestedIncludesTree->intersect($allowedRequestIncludesTree), $requestedExcludesTree->intersect($allowedRequestExcludesTree), $requestedOnlyTree->intersect($allowedRequestOnlyTree), diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 5b9963b1..f332b723 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Contracts\WrappableData; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; @@ -101,17 +102,17 @@ protected function shouldIncludeProperty( // Lazy excluded checks - if ($context->lazyExcluded instanceof AllTreeNode) { + if ($context->partials->lazyExcluded instanceof AllTreeNode) { return false; } - if ($context->lazyExcluded instanceof PartialTreeNode && $context->lazyExcluded->hasField($name)) { + if ($context->partials->lazyExcluded instanceof PartialTreeNode && $context->partials->lazyExcluded->hasField($name)) { return false; } // Lazy included checks - if ($context->lazyIncluded instanceof AllTreeNode) { + if ($context->partials->lazyIncluded instanceof AllTreeNode) { return true; } @@ -119,38 +120,38 @@ protected function shouldIncludeProperty( return true; } - return $context->lazyIncluded instanceof PartialTreeNode && $context->lazyIncluded->hasField($name); + return $context->partials->lazyIncluded instanceof PartialTreeNode && $context->partials->lazyIncluded->hasField($name); } protected function isPropertyHidden( string $name, TransformationContext $context ): bool { - if ($context->except instanceof AllTreeNode) { + if ($context->partials->except instanceof AllTreeNode) { return true; } if ( - $context->except instanceof PartialTreeNode - && $context->except->hasField($name) - && $context->except->getNested($name) instanceof ExcludedTreeNode + $context->partials->except instanceof PartialTreeNode + && $context->partials->except->hasField($name) + && $context->partials->except->getNested($name) instanceof ExcludedTreeNode ) { return true; } - if ($context->except instanceof PartialTreeNode) { + if ($context->partials->except instanceof PartialTreeNode) { return false; } - if ($context->only instanceof AllTreeNode) { + if ($context->partials->only instanceof AllTreeNode) { return false; } - if ($context->only instanceof PartialTreeNode && $context->only->hasField($name)) { + if ($context->partials->only instanceof PartialTreeNode && $context->partials->only->hasField($name)) { return false; } - if ($context->only instanceof PartialTreeNode || $context->only instanceof ExcludedTreeNode) { + if ($context->partials->only instanceof PartialTreeNode || $context->partials->only instanceof ExcludedTreeNode) { return true; } @@ -172,12 +173,12 @@ protected function resolvePropertyValue( $nextContext = $context->next($property->name); - if (is_array($value) && ($nextContext->only instanceof AllTreeNode || $nextContext->only instanceof PartialTreeNode)) { - return Arr::only($value, $nextContext->only->getFields()); + if (is_array($value) && ($nextContext->partials->only instanceof AllTreeNode || $nextContext->partials->only instanceof PartialTreeNode)) { + return Arr::only($value, $nextContext->partials->only->getFields()); } - if (is_array($value) && ($nextContext->except instanceof AllTreeNode || $nextContext->except instanceof PartialTreeNode)) { - return Arr::except($value, $nextContext->except->getFields()); + if (is_array($value) && ($nextContext->partials->except instanceof AllTreeNode || $nextContext->partials->except instanceof PartialTreeNode)) { + return Arr::except($value, $nextContext->partials->except->getFields()); } if ($transformer = $this->resolveTransformerForValue($property, $value, $nextContext)) { @@ -189,13 +190,13 @@ protected function resolvePropertyValue( && $value instanceof TransformableData && $nextContext->transformValues ) { - $wrapExecutionType = match ($context->wrapExecutionType){ + $wrapExecutionType = match ($context->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::Enabled, WrapExecutionType::Disabled => WrapExecutionType::Disabled, WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled }; - return $value->transform2($nextContext->setWrapExecutionType($wrapExecutionType)); + return $value->transform($nextContext->setWrapExecutionType($wrapExecutionType)); } if ( @@ -203,21 +204,21 @@ protected function resolvePropertyValue( && $value instanceof TransformableData && $nextContext->transformValues ) { - $wrapExecutionType = match ($context->wrapExecutionType){ + $wrapExecutionType = match ($context->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::TemporarilyDisabled, WrapExecutionType::Disabled => WrapExecutionType::Disabled, WrapExecutionType::TemporarilyDisabled => WrapExecutionType::TemporarilyDisabled }; - return $value->transform2($nextContext->setWrapExecutionType($wrapExecutionType)); + return $value->transform($nextContext->setWrapExecutionType($wrapExecutionType)); } if ( - $property->type->isDataCollectable + $property->type->kind->isDataCollectable() && is_iterable($value) && $nextContext->transformValues ) { - $wrapExecutionType = match ($context->wrapExecutionType){ + $wrapExecutionType = match ($context->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::Enabled, WrapExecutionType::Disabled => WrapExecutionType::Disabled, WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled @@ -228,6 +229,7 @@ protected function resolvePropertyValue( $nextContext->setWrapExecutionType($wrapExecutionType) ); } + return $value; } @@ -243,7 +245,7 @@ protected function resolveTransformerForValue( $transformer = $property->transformer ?? $this->dataConfig->findGlobalTransformerForValue($value); $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer - && ($property->type->isDataObject || $property->type->isDataCollectable); + && $property->type->kind !== DataTypeKind::Default; if ($shouldUseDefaultDataTransformer) { return null; diff --git a/src/Resource.php b/src/Resource.php new file mode 100644 index 00000000..d87a20d1 --- /dev/null +++ b/src/Resource.php @@ -0,0 +1,29 @@ +type->isDataCollectable && $rules->hasType(Present::class)) { + if ($property->type->kind->isDataCollectable() && $rules->hasType(Present::class)) { return false; } diff --git a/src/Support/AllowedPartialsParser.php b/src/Support/AllowedPartialsParser.php index c5f011d2..69f708bb 100644 --- a/src/Support/AllowedPartialsParser.php +++ b/src/Support/AllowedPartialsParser.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData\Support; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\TreeNodes\AllTreeNode; use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; use Spatie\LaravelData\Support\TreeNodes\PartialTreeNode; @@ -29,7 +30,7 @@ public function execute( /** @var \Spatie\LaravelData\Support\DataProperty $dataProperty */ $dataProperty = $dataClass->properties->get($field); - if ($dataProperty->type->isDataObject || $dataProperty->type->isDataCollectable) { + if ($dataProperty->type->kind !== DataTypeKind::Default) { return [ $field => $this->execute( $type, diff --git a/src/Support/Annotations/DataCollectableAnnotation.php b/src/Support/Annotations/DataCollectableAnnotation.php new file mode 100644 index 00000000..4b89548e --- /dev/null +++ b/src/Support/Annotations/DataCollectableAnnotation.php @@ -0,0 +1,14 @@ + */ + protected static array $contexts = []; + + public static function create(): self + { + return new self(); + } + + /** @return array */ + public function getForClass(ReflectionClass $class): array + { + return collect($this->get($class))->keyBy(fn(DataCollectableAnnotation $annotation) => $annotation->property)->all(); + } + + public function getForProperty(ReflectionProperty $property): ?DataCollectableAnnotation + { + return Arr::first($this->get($property)); + } + + /** @return \Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation[] */ + protected function get( + ReflectionProperty|ReflectionClass $reflection + ): array { + $comment = $reflection->getDocComment(); + + if ($comment === false) { + return []; + } + + $comment = str_replace('?', '', $comment); + + $kindPattern = '(?:@property|@var)\s*'; + $fqsenPattern = '[\\\\a-z0-9_\|]+'; + $keyPattern = '(?:int|string|\(int\|string\)|array-key)'; + $parameterPattern = '\s*\$?(?[a-z0-9_]+)?'; + + preg_match_all( + "/{$kindPattern}(?{$fqsenPattern})\[\]{$parameterPattern}/i", + $comment, + $arrayMatches, + ); + + preg_match_all( + "/{$kindPattern}(?{$fqsenPattern})<(?:{$keyPattern},\s*)?(?{$fqsenPattern})>{$parameterPattern}/i", + $comment, + $collectionMatches, + ); + + return [ + ...$this->resolveArrayAnnotations($reflection, $arrayMatches), + ...$this->resolveCollectionAnnotations($reflection, $collectionMatches), + ]; + } + + protected function resolveArrayAnnotations( + ReflectionProperty|ReflectionClass $reflection, + array $arrayMatches + ): array { + $annotations = []; + + foreach ($arrayMatches['dataClass'] as $index => $dataClass) { + $parameter = $arrayMatches['parameter'][$index]; + + $dataClass = $this->resolveDataClass($reflection, $dataClass); + + if ($dataClass === null) { + continue; + } + + $annotations[] = new DataCollectableAnnotation( + $dataClass, + null, + empty($parameter) ? null : $parameter + ); + } + + return $annotations; + } + + protected function resolveCollectionAnnotations( + ReflectionProperty|ReflectionClass $reflection, + array $collectionMatches + ): array { + $annotations = []; + + foreach ($collectionMatches['dataClass'] as $index => $dataClass) { + $parameter = $collectionMatches['parameter'][$index]; + + $dataClass = $this->resolveDataClass($reflection, $dataClass); + + if ($dataClass === null) { + continue; + } + + $annotations[] = new DataCollectableAnnotation( + $dataClass, + null, + empty($parameter) ? null : $parameter + ); + } + + return $annotations; + } + + protected function resolveDataClass( + ReflectionProperty|ReflectionClass $reflection, + string $class + ): ?string { + if (str_contains($class, '|')) { + foreach (explode('|', $class) as $explodedClass) { + if ($foundClass = $this->resolveDataClass($reflection, $explodedClass)) { + return $foundClass; + } + } + + return null; + } + + $class = ltrim($class, '\\'); + + if (is_subclass_of($class, BaseData::class)) { + return $class; + } + + $class = $this->resolveFcqn($reflection, $class); + + if (is_subclass_of($class, BaseData::class)) { + return $class; + } + + return null; + } + + protected function resolveCollectionClass( + ReflectionProperty|ReflectionClass $reflection, + string $class + ): ?string { + if (str_contains($class, '|')) { + foreach (explode('|', $class) as $explodedClass) { + if ($foundClass = $this->resolveCollectionClass($reflection, $explodedClass)) { + return $foundClass; + } + } + + return null; + } + + if ($class === 'array') { + return $class; + } + + $class = ltrim($class, '\\'); + + if (is_a($class, BaseDataCollectable::class, true) + || is_a($class, Enumerable::class, true) + || is_a($class, AbstractPaginator::class, true) + || is_a($class, CursorPaginator::class, true) + ) { + return $class; + } + + $class = $this->resolveFcqn($reflection, $class); + + if (is_a($class, BaseDataCollectable::class, true) + || is_a($class, Enumerable::class, true) + || is_a($class, AbstractPaginator::class, true) + || is_a($class, CursorPaginator::class, true)) { + return $class; + } + + return null; + } + + protected function resolveFcqn( + ReflectionProperty|ReflectionClass $reflection, + string $class + ): ?string { + $context = $this->getContext($reflection); + + $type = (new FqsenResolver())->resolve($class, $context); + + return ltrim((string) $type, '\\'); + } + + + protected function getContext(ReflectionProperty|ReflectionClass $reflection): Context + { + $reflectionClass = $reflection instanceof ReflectionProperty + ? $reflection->getDeclaringClass() + : $reflection; + + return static::$contexts[$reflectionClass->getName()] ??= (new ContextFactory())->createFromReflector($reflectionClass); + } +} diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index d03547da..5d30d490 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -17,12 +17,14 @@ use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Mappers\ProvidedNameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; /** * @property class-string $name * @property Collection $properties * @property Collection $methods * @property Collection $attributes + * @property array $dataCollectablePropertyAnnotations */ class DataClass { @@ -39,23 +41,27 @@ public function __construct( public readonly bool $validateable, public readonly bool $wrappable, public readonly Collection $attributes, + public readonly array $dataCollectablePropertyAnnotations ) { } public static function create(ReflectionClass $class): self { $attributes = collect($class->getAttributes()) - ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) - ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + ->filter(fn(ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn(ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); $methods = collect($class->getMethods()); - $constructor = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor()); + $constructor = $methods->first(fn(ReflectionMethod $method) => $method->isConstructor()); + + $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); $properties = self::resolveProperties( $class, $constructor, - NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes) + NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes), + $dataCollectablePropertyAnnotations, ); return new self( @@ -70,7 +76,8 @@ public static function create(ReflectionClass $class): self transformable: $class->implementsInterface(TransformableData::class), validateable: $class->implementsInterface(ValidateableData::class), wrappable: $class->implementsInterface(WrappableData::class), - attributes: $attributes, + attributes: $attributes, + dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations ); } @@ -78,9 +85,9 @@ protected static function resolveMethods( ReflectionClass $reflectionClass, ): Collection { return collect($reflectionClass->getMethods()) - ->filter(fn (ReflectionMethod $method) => str_starts_with($method->name, 'from')) + ->filter(fn(ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collectFrom')) ->mapWithKeys( - fn (ReflectionMethod $method) => [$method->name => DataMethod::create($method)], + fn(ReflectionMethod $method) => [$method->name => DataMethod::create($method)], ); } @@ -88,19 +95,21 @@ protected static function resolveProperties( ReflectionClass $class, ?ReflectionMethod $constructorMethod, array $mappers, + array $dataCollectablePropertyAnnotations, ): Collection { $defaultValues = self::resolveDefaultValues($class, $constructorMethod); return collect($class->getProperties(ReflectionProperty::IS_PUBLIC)) - ->reject(fn (ReflectionProperty $property) => $property->isStatic()) + ->reject(fn(ReflectionProperty $property) => $property->isStatic()) ->values() - ->mapWithKeys(fn (ReflectionProperty $property) => [ + ->mapWithKeys(fn(ReflectionProperty $property) => [ $property->name => DataProperty::create( $property, array_key_exists($property->getName(), $defaultValues), $defaultValues[$property->getName()] ?? null, $mappers['inputNameMapper'], $mappers['outputNameMapper'], + $dataCollectablePropertyAnnotations[$property->getName()] ?? null, ), ]); } @@ -114,8 +123,8 @@ protected static function resolveDefaultValues( } $values = collect($constructorMethod->getParameters()) - ->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) - ->mapWithKeys(fn (ReflectionParameter $parameter) => [ + ->filter(fn(ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) + ->mapWithKeys(fn(ReflectionParameter $parameter) => [ $parameter->name => $parameter->getDefaultValue(), ]) ->toArray(); diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index d3c2d6f7..1f3fe2ec 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -3,8 +3,12 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; +use ReflectionIntersectionType; use ReflectionMethod; +use ReflectionNamedType; use ReflectionParameter; +use ReflectionUnionType; +use Spatie\LaravelData\Enums\CustomCreationMethodType; /** * @property Collection $parameters @@ -16,26 +20,24 @@ public function __construct( public readonly Collection $parameters, public readonly bool $isStatic, public readonly bool $isPublic, - public readonly bool $isCustomCreationMethod, + public readonly CustomCreationMethodType $customCreationMethodType, + public readonly ?Type $returnType, ) { } public static function create(ReflectionMethod $method): self { - $isCustomCreationMethod = $method->isStatic() - && $method->isPublic() - && str_starts_with($method->getName(), 'from') - && $method->name !== 'from' - && $method->name !== 'optional'; + $returnType = Type::create($method->getReturnType()); return new self( $method->name, collect($method->getParameters())->map( - fn (ReflectionParameter $parameter) => DataParameter::create($parameter), + fn(ReflectionParameter $parameter) => DataParameter::create($parameter), ), $method->isStatic(), $method->isPublic(), - $isCustomCreationMethod + self::resolveCustomCreationMethodType($method, $returnType), + $returnType ); } @@ -58,16 +60,41 @@ public static function createConstructor(?ReflectionMethod $method, Collection $ $parameters, false, $method->isPublic(), - false + CustomCreationMethodType::None, + null, ); } + protected static function resolveCustomCreationMethodType( + ReflectionMethod $method, + ?Type $returnType, + ): CustomCreationMethodType { + if (! $method->isStatic() + || ! $method->isPublic() + || $method->name === 'from' + || $method->name === 'collect' + || $method->name === 'collection' + ) { + return CustomCreationMethodType::None; + } + + if (str_starts_with($method->name, 'from')) { + return CustomCreationMethodType::Object; + } + + if (str_starts_with($method->name, 'collect') && $returnType && count($returnType) > 0) { + return CustomCreationMethodType::Collection; + } + + return CustomCreationMethodType::None; + } + public function accepts(mixed ...$input): bool { /** @var Collection<\Spatie\LaravelData\Support\DataParameter|\Spatie\LaravelData\Support\DataProperty> $parameters */ $parameters = array_is_list($input) ? $this->parameters - : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); + : $this->parameters->mapWithKeys(fn(DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); if (count($input) > $parameters->count()) { return false; @@ -91,4 +118,9 @@ public function accepts(mixed ...$input): bool return true; } + + public function returns(string $type): bool + { + return $this->returnType->acceptsType($type); + } } diff --git a/src/Support/DataParameter.php b/src/Support/DataParameter.php index be9e4281..e677b97e 100644 --- a/src/Support/DataParameter.php +++ b/src/Support/DataParameter.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support; use ReflectionParameter; +use Spatie\LaravelData\Support\Factories\DataTypeFactory; class DataParameter { @@ -25,7 +26,7 @@ public static function create( $parameter->isPromoted(), $hasDefaultValue, $hasDefaultValue ? $parameter->getDefaultValue() : null, - DataType::create($parameter), + DataTypeFactory::create()->build($parameter), ); } } diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index f1e45856..60f5871a 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -11,6 +11,9 @@ use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Mappers\NameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; +use Spatie\LaravelData\Support\Factories\DataTypeFactory; use Spatie\LaravelData\Transformers\Transformer; /** @@ -41,6 +44,7 @@ public static function create( mixed $defaultValue = null, ?NameMapper $classInputNameMapper = null, ?NameMapper $classOutputNameMapper = null, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, ): self { $attributes = collect($property->getAttributes()) ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) @@ -63,7 +67,7 @@ public static function create( return new self( name: $property->name, className: $property->class, - type: DataType::create($property), + type: DataTypeFactory::create()->build($property, $classDefinedDataCollectableAnnotation), validate: ! $attributes->contains(fn (object $attribute) => $attribute instanceof WithoutValidation), isPromoted: $property->isPromoted(), isReadonly: $property->isReadOnly(), diff --git a/src/Support/DataType.php b/src/Support/DataType.php index 00c0cf0f..cbc4cb24 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -3,6 +3,11 @@ namespace Spatie\LaravelData\Support; use Countable; +use Illuminate\Contracts\Pagination\CursorPaginator; +use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Support\Arr; +use Illuminate\Support\Collection; +use IteratorAggregate; use ReflectionIntersectionType; use ReflectionNamedType; use ReflectionParameter; @@ -13,254 +18,30 @@ use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataCollectableType; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotFindDataClass; use Spatie\LaravelData\Exceptions\InvalidDataType; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; use TypeError; +use function Pest\Laravel\instance; -class DataType implements Countable +/** + * @property class-string|null $dataClass + */ +class DataType extends Type { - public readonly bool $isNullable; - - public readonly bool $isMixed; - - public readonly bool $isLazy; - - public readonly bool $isOptional; - - public readonly bool $isDataObject; - - public readonly bool $isDataCollectable; - - public readonly ?DataCollectableType $dataCollectableType; - - /** @var class-string|null */ - public readonly ?string $dataClass; - - public readonly array $acceptedTypes; - - public static function create(ReflectionParameter|ReflectionProperty $reflection): self - { - return new self($reflection); - } - - public function __construct(ReflectionParameter|ReflectionProperty $reflection) - { - $type = $reflection->getType(); - - if ($type === null) { - $this->acceptedTypes = []; - $this->isNullable = true; - $this->isMixed = true; - $this->isLazy = false; - $this->isOptional = false; - $this->isDataObject = false; - $this->isDataCollectable = false; - $this->dataCollectableType = null; - $this->dataClass = null; - - return; - } - - if ($type instanceof ReflectionNamedType) { - if (is_a($type->getName(), Lazy::class, true)) { - throw InvalidDataType::onlyLazy($reflection); - } - - if (is_a($type->getName(), Optional::class, true)) { - throw InvalidDataType::onlyOptional($reflection); - } - - $this->isNullable = $type->allowsNull(); - $this->isMixed = $type->getName() === 'mixed'; - $this->acceptedTypes = $this->isMixed - ? [] - : [ - $type->getName() => $this->resolveBaseTypes($type->getName()), - ]; - $this->isLazy = false; - $this->isOptional = false; - $this->isDataObject = is_a($type->getName(), BaseData::class, true); - $this->dataCollectableType = $this->resolveDataCollectableType($type); - $this->isDataCollectable = $this->dataCollectableType !== null; - - $this->dataClass = match (true) { - $this->isDataObject => $type->getName(), - $this->isDataCollectable => $this->resolveDataCollectableClass($reflection), - default => null - }; - - return; - } - - if (! ($type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType)) { - throw new TypeError('Invalid reflection type'); - } - - $acceptedTypes = []; - $isNullable = false; - $isMixed = false; - $isLazy = false; - $isOptional = false; - $isDataObject = false; - $dataCollectableType = null; - - foreach ($type->getTypes() as $namedType) { - if ($namedType->getName() !== 'null' - && ! is_a($namedType->getName(), Lazy::class, true) - && ! is_a($namedType->getName(), Optional::class, true) - ) { - $acceptedTypes[$namedType->getName()] = $this->resolveBaseTypes($namedType->getName()); - } - - $isNullable = $isNullable || $namedType->allowsNull(); - $isMixed = $namedType->getName() === 'mixed'; - $isLazy = $isLazy || is_a($namedType->getName(), Lazy::class, true); - $isOptional = $isOptional || is_a($namedType->getName(), Optional::class, true); - $isDataObject = $isDataObject || is_a($namedType->getName(), BaseData::class, true); - $dataCollectableType = $dataCollectableType ?? $this->resolveDataCollectableType($namedType); - } - - $this->acceptedTypes = $acceptedTypes; - $this->isNullable = $isNullable; - $this->isMixed = $isMixed; - $this->isLazy = $isLazy; - $this->isOptional = $isOptional; - $this->isDataObject = $isDataObject; - $this->dataCollectableType = $dataCollectableType; - $this->isDataCollectable = $this->dataCollectableType !== null; - - if ($this->isDataObject && count($this->acceptedTypes) > 1) { - throw InvalidDataType::unionWithData($reflection); - } - - if ($this->isDataCollectable && count($this->acceptedTypes) > 1) { - throw InvalidDataType::unionWithDataCollection($reflection); - } - - $this->dataClass = match (true) { - $this->isDataObject => array_key_first($acceptedTypes), - $this->isDataCollectable => $this->resolveDataCollectableClass($reflection), - default => null - }; - } - - public function isEmpty(): bool - { - return $this->count() === 0; - } - - public function count(): int - { - return count($this->acceptedTypes); - } - - public function acceptsValue(mixed $value): bool - { - if ($this->isNullable && $value === null) { - return true; - } - - $type = gettype($value); - - $type = match ($type) { - 'integer' => 'int', - 'boolean' => 'bool', - 'double' => 'float', - 'object' => $value::class, - default => $type, - }; - - return $this->acceptsType($type); - } - - public function acceptsType(string $type): bool - { - if ($this->isMixed) { - return true; - } - - if (array_key_exists($type, $this->acceptedTypes)) { - return true; - } - - if (in_array($type, ['string', 'int', 'bool', 'float', 'array'])) { - return false; - } - - foreach ([$type, ...$this->resolveBaseTypes($type)] as $givenType) { - if (array_key_exists($givenType, $this->acceptedTypes)) { - return true; - } - } - - return false; - } - - public function findAcceptedTypeForBaseType(string $class): ?string - { - foreach ($this->acceptedTypes as $acceptedType => $acceptedBaseTypes) { - if ($class === $acceptedType) { - return $acceptedType; - } - - if (in_array($class, $acceptedBaseTypes)) { - return $acceptedType; - } - } - - return null; - } - - protected function resolveBaseTypes(string $type): array - { - if (! class_exists($type)) { - return []; - } - - return array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]); - } - - protected function resolveDataCollectableClass( - ReflectionProperty|ReflectionParameter $reflection, - ): ?string { - $attributes = $reflection->getAttributes(DataCollectionOf::class); - - if (! empty($attributes)) { - return $attributes[0]->getArguments()[0]; - } - - if ($reflection instanceof ReflectionParameter) { - return null; - } - - $class = (new DataCollectionAnnotationReader())->getClass($reflection); - - if ($class === null) { - throw CannotFindDataClass::wrongDataCollectionAnnotation( - $reflection->class, - $reflection->name - ); - } - - return $class; - } - - protected function resolveDataCollectableType( - ReflectionNamedType $reflection, - ): ?DataCollectableType { - $className = $reflection->getName(); - - return match (true) { - is_a($className, DataCollection::class, true) => DataCollectableType::Default, - is_a($className, PaginatedDataCollection::class, true) => DataCollectableType::Paginated, - is_a($className, CursorPaginatedDataCollection::class, true) => DataCollectableType::CursorPaginated, - default => null, - }; + public function __construct( + bool $isNullable, + bool $isMixed, + public readonly bool $isLazy, + public readonly bool $isOptional, + public readonly DataTypeKind $kind, + public readonly ?string $dataClass, + public readonly ?string $dataCollectableClass, + array $acceptedTypes, + ) { + parent::__construct($isNullable, $isMixed, $acceptedTypes); } } diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php new file mode 100644 index 00000000..69e91720 --- /dev/null +++ b/src/Support/Factories/DataTypeFactory.php @@ -0,0 +1,281 @@ +getType(); + + return match (true) { + $type === null => $this->buildForEmptyType(), + $type instanceof ReflectionNamedType => $this->buildForNamedType( + $property, + $type, + $classDefinedDataCollectableAnnotation + ), + $type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType => $this->buildForMultiType( + $property, + $type, + $classDefinedDataCollectableAnnotation + ), + default => throw new TypeError('Invalid reflection type') + }; + } + + protected function buildForEmptyType(): DataType + { + $type = DataType::create(null); + + return new DataType( + isNullable: $type->isNullable, + isMixed: $type->isMixed, + isLazy: false, + isOptional: false, + kind: DataTypeKind::Default, + dataClass: null, + dataCollectableClass: null, + acceptedTypes: $type->acceptedTypes, + ); + } + + protected function buildForNamedType( + ReflectionParameter|ReflectionProperty $reflectionProperty, + ReflectionNamedType $reflectionType, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + ): DataType { + $typeName = $reflectionType->getName(); + + if (is_a($typeName, Lazy::class, true)) { + throw InvalidDataType::onlyLazy($reflectionProperty); + } + + if (is_a($typeName, Optional::class, true)) { + throw InvalidDataType::onlyOptional($reflectionProperty); + } + + $type = DataType::create($reflectionType); + + $kind = DataTypeKind::Default; + $dataClass = null; + $dataCollectableClass = null; + + if (! $type->isMixed) { + [ + 'kind' => $kind, + 'dataClass' => $dataClass, + 'dataCollectableClass' => $dataCollectableClass, + ] = $this->resolveDataSpecificProperties( + $reflectionProperty, + $reflectionType, + $type->acceptedTypes[$typeName], + $classDefinedDataCollectableAnnotation + ); + } + + return new DataType( + isNullable: $reflectionType->allowsNull(), + isMixed: $type->isMixed, + isLazy: false, + isOptional: false, + kind: $kind, + dataClass: $dataClass, + dataCollectableClass: $dataCollectableClass, + acceptedTypes: $type->acceptedTypes, + ); + } + + protected function buildForMultiType( + ReflectionParameter|ReflectionProperty $reflectionProperty, + ReflectionUnionType|ReflectionIntersectionType $multiReflectionType, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + ): DataType { + $acceptedTypes = []; + $isNullable = false; + $isLazy = false; + $isOptional = false; + $isMixed = false; + $kind = DataTypeKind::Default; + $dataClass = null; + $dataCollectableClass = null; + + foreach ($multiReflectionType->getTypes() as $reflectionType) { + $typeName = $reflectionType->getName(); + + $singleType = Type::create($reflectionType); + + $singleTypeIsLazy = is_a($typeName, Lazy::class, true); + $singleTypeIsOptional = is_a($typeName, Optional::class, true); + + $isNullable = $isNullable || $singleType->isNullable; + $isMixed = $isMixed || $singleType->isMixed; + $isLazy = $isLazy || $singleTypeIsLazy; + $isOptional = $isOptional || $singleTypeIsOptional; + + if ($typeName + && array_key_exists($typeName, $singleType->acceptedTypes) + && ! $singleTypeIsLazy + && ! $singleTypeIsOptional + ) { + $acceptedTypes[$typeName] = $singleType->acceptedTypes[$typeName]; + + if ($kind !== DataTypeKind::Default) { + continue; + } + + [ + 'kind' => $kind, + 'dataClass' => $dataClass, + 'dataCollectableClass' => $dataCollectableClass, + ] = $this->resolveDataSpecificProperties( + $reflectionProperty, + $reflectionType, + $singleType->acceptedTypes[$typeName], + $classDefinedDataCollectableAnnotation + ); + } + } + + if ($kind->isDataObject() && count($acceptedTypes) > 1) { + throw InvalidDataType::unionWithData($reflectionProperty); + } + + if ($kind->isDataCollectable() && count($acceptedTypes) > 1) { + throw InvalidDataType::unionWithDataCollection($reflectionProperty); + } + + return new DataType( + isNullable: $isNullable, + isMixed: false, + isLazy: $isLazy, + isOptional: $isOptional, + kind: $kind, + dataClass: $dataClass, + dataCollectableClass: $dataCollectableClass, + acceptedTypes: $acceptedTypes + ); + } + + protected function resolveDataSpecificProperties( + ReflectionParameter|ReflectionProperty $reflectionProperty, + ReflectionNamedType $reflectionType, + array $baseTypes, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + ): array { + $typeName = $reflectionType->getName(); + + $kind = match (true) { + in_array(BaseData::class, $baseTypes) => DataTypeKind::DataObject, + $typeName === 'array' => DataTypeKind::Array, + in_array(Enumerable::class, $baseTypes) => DataTypeKind::Enumerable, + in_array(DataCollection::class, $baseTypes) || $typeName === DataCollection::class => DataTypeKind::DataCollection, + in_array(PaginatedDataCollection::class, $baseTypes) || $typeName === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, + in_array(CursorPaginatedDataCollection::class, $baseTypes) || $typeName === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, + in_array(Paginator::class, $baseTypes) || in_array(AbstractPaginator::class, $baseTypes) => DataTypeKind::Paginator, + in_array(CursorPaginator::class, $baseTypes) || in_array(AbstractCursorPaginator::class, $baseTypes) => DataTypeKind::CursorPaginator, + default => DataTypeKind::Default, + }; + + if ($kind === DataTypeKind::Default) { + return [ + 'kind' => DataTypeKind::Default, + 'dataClass' => null, + 'dataCollectableClass' => null, + ]; + } + + if ($kind === DataTypeKind::DataObject) { + return [ + 'kind' => DataTypeKind::DataObject, + 'dataClass' => $typeName, + 'dataCollectableClass' => null, + ]; + } + + $dataClass = null; + + $attributes = $reflectionProperty instanceof ReflectionProperty + ? $reflectionProperty->getAttributes(DataCollectionOf::class) + : []; + + if ($attribute = Arr::first($attributes)) { + $dataClass = $attribute->getArguments()[0]; + } + + $dataClass ??= $classDefinedDataCollectableAnnotation?->dataClass; + + $dataClass ??= $reflectionProperty instanceof ReflectionProperty + ? DataCollectableAnnotationReader::create()->getForProperty($reflectionProperty)?->dataClass + : null; + + if ($dataClass !== null) { + return [ + 'kind' => $kind, + 'dataClass' => $dataClass, + 'dataCollectableClass' => $typeName, + ]; + } + + if (in_array($kind, [DataTypeKind::Array, DataTypeKind::Paginator, DataTypeKind::CursorPaginator, DataTypeKind::Enumerable])) { + return [ + 'kind' => DataTypeKind::Default, + 'dataClass' => null, + 'dataCollectableClass' => null, + ]; + } + + throw CannotFindDataClass::missingDataCollectionAnotation( + $reflectionProperty instanceof ReflectionProperty ? $reflectionProperty->class : 'unknown', + $reflectionProperty->name + ); + } + + protected function resolveBaseTypes(string $type): array + { + if (! class_exists($type)) { + return []; + } + + return array_unique([ + ...array_values(class_parents($type)), + ...array_values(class_implements($type)), + ]); + } +} diff --git a/src/Support/PartialTrees.php b/src/Support/PartialTrees.php deleted file mode 100644 index a083e714..00000000 --- a/src/Support/PartialTrees.php +++ /dev/null @@ -1,40 +0,0 @@ -lazyIncluded->getNested($field), - $this->lazyExcluded->getNested($field), - $this->only->getNested($field), - $this->except->getNested($field), - ); - } - - public function __toString() - { - return '{' . PHP_EOL . collect([ - 'lazyIncluded' => $this->lazyIncluded, - 'lazyExcluded' => $this->lazyExcluded, - 'only' => $this->only, - 'except' => $this->except, - ]) - ->map(fn (TreeNode $node, string $type) => "\"{$type}\":{$node}") - ->join(',' . PHP_EOL) . PHP_EOL . '}'; - } -} diff --git a/src/Support/Partials/ForwardsToPartialsDefinition.php b/src/Support/Partials/ForwardsToPartialsDefinition.php new file mode 100644 index 00000000..ec9d68ae --- /dev/null +++ b/src/Support/Partials/ForwardsToPartialsDefinition.php @@ -0,0 +1,74 @@ +getPartialsDefinition()->includes[$include] = true; + } + + return $this; + } + + public function exclude(string ...$excludes): static + { + foreach ($excludes as $exclude) { + $this->getPartialsDefinition()->excludes[$exclude] = true; + } + + return $this; + } + + public function only(string ...$only): static + { + foreach ($only as $onlyDefinition) { + $this->getPartialsDefinition()->only[$onlyDefinition] = true; + } + + return $this; + } + + public function except(string ...$except): static + { + foreach ($except as $exceptDefinition) { + $this->getPartialsDefinition()->except[$exceptDefinition] = true; + } + + return $this; + } + + public function includeWhen(string $include, bool|Closure $condition): static + { + $this->getPartialsDefinition()->includes[$include] = $condition; + + return $this; + } + + public function excludeWhen(string $exclude, bool|Closure $condition): static + { + $this->getPartialsDefinition()->excludes[$exclude] = $condition; + + return $this; + } + + public function onlyWhen(string $only, bool|Closure $condition): static + { + $this->getPartialsDefinition()->only[$only] = $condition; + + return $this; + } + + public function exceptWhen(string $except, bool|Closure $condition): static + { + $this->getPartialsDefinition()->except[$except] = $condition; + + return $this; + } +} diff --git a/src/Support/Partials/PartialsDefinition.php b/src/Support/Partials/PartialsDefinition.php new file mode 100644 index 00000000..bafafec1 --- /dev/null +++ b/src/Support/Partials/PartialsDefinition.php @@ -0,0 +1,23 @@ + $includes + * @param array $excludes + * @param array $only + * @param array $except + */ + public function __construct( + public array $includes = [], + public array $excludes = [], + public array $only = [], + public array $except = [], + ) + { + } +} diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index 53db86bd..30bef46c 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -3,22 +3,13 @@ namespace Spatie\LaravelData\Support\Transformation; use Closure; +use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Wrapping\Wrap; class DataContext { - /** - * @param array $includes - * @param array $excludes - * @param array $only - * @param array $except - * @param \Spatie\LaravelData\Support\Wrapping\Wrap|null $wrap - */ public function __construct( - public array $includes = [], - public array $excludes = [], - public array $only = [], - public array $except = [], + public PartialsDefinition $partialsDefinition = new PartialsDefinition(), public ?Wrap $wrap = null, ) { } diff --git a/src/Support/Transformation/LocalTransformationContext.php b/src/Support/Transformation/LocalTransformationContext.php deleted file mode 100644 index 8aeacb76..00000000 --- a/src/Support/Transformation/LocalTransformationContext.php +++ /dev/null @@ -1,51 +0,0 @@ -getDataContext(); - - $filter = fn (bool|null|Closure $condition, string $definition) => match (true) { - is_bool($condition) => $condition, - $condition === null => false, - is_callable($condition) => $condition($data), - }; - - return new self( - app(PartialsParser::class)->execute( - collect($dataContext->includes)->filter($filter)->keys()->all() - ), - app(PartialsParser::class)->execute( - collect($dataContext->excludes)->filter($filter)->keys()->all() - ), - app(PartialsParser::class)->execute( - collect($dataContext->only)->filter($filter)->keys()->all() - ), - app(PartialsParser::class)->execute( - collect($dataContext->except)->filter($filter)->keys()->all() - ), - ); - } -} diff --git a/src/Support/Transformation/PartialTransformationContext.php b/src/Support/Transformation/PartialTransformationContext.php new file mode 100644 index 00000000..dc166172 --- /dev/null +++ b/src/Support/Transformation/PartialTransformationContext.php @@ -0,0 +1,71 @@ + match (true) { + is_bool($condition) => $condition, + $condition === null => false, + is_callable($condition) => $condition($data), + }; + + return new self( + app(PartialsParser::class)->execute( + collect($partialsDefinition->includes)->filter($filter)->keys()->all() + ), + app(PartialsParser::class)->execute( + collect($partialsDefinition->excludes)->filter($filter)->keys()->all() + ), + app(PartialsParser::class)->execute( + collect($partialsDefinition->only)->filter($filter)->keys()->all() + ), + app(PartialsParser::class)->execute( + collect($partialsDefinition->except)->filter($filter)->keys()->all() + ), + ); + } + + public function merge(self $other): self + { + return new self( + $this->lazyIncluded->merge($other->lazyIncluded), + $this->lazyExcluded->merge($other->lazyExcluded), + $this->only->merge($other->only), + $this->except->merge($other->except), + ); + } + + public function getNested(string $field): self + { + return new self( + $this->lazyIncluded->getNested($field), + $this->lazyExcluded->getNested($field), + $this->only->getNested($field), + $this->except->getNested($field), + ); + } +} diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index d684d256..be77e152 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -18,10 +18,7 @@ public function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public TreeNode $lazyIncluded = new DisabledTreeNode(), - public TreeNode $lazyExcluded = new DisabledTreeNode(), - public TreeNode $only = new DisabledTreeNode(), - public TreeNode $except = new DisabledTreeNode(), + public PartialTransformationContext $partials = new PartialTransformationContext(), ) { } @@ -32,23 +29,17 @@ public function next( $this->transformValues, $this->mapPropertyNames, $this->wrapExecutionType, - $this->lazyIncluded->getNested($property), - $this->lazyExcluded->getNested($property), - $this->only->getNested($property), - $this->except->getNested($property), + $this->partials->getNested($property) ); } - public function merge(LocalTransformationContext $localContext): self + public function mergePartials(PartialTransformationContext $partials): self { return new self( $this->transformValues, $this->mapPropertyNames, $this->wrapExecutionType, - $this->lazyIncluded->merge($localContext->lazyIncluded), - $this->lazyExcluded->merge($localContext->lazyExcluded), - $this->only->merge($localContext->only), - $this->except->merge($localContext->except), + $this->partials->merge($partials), ); } @@ -58,10 +49,7 @@ public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self $this->transformValues, $this->mapPropertyNames, $wrapExecutionType, - $this->lazyIncluded, - $this->lazyExcluded, - $this->only, - $this->except, + $this->partials, ); } } diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index e9b227f6..270b858c 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -4,8 +4,11 @@ use Closure; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; +use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\PartialsParser; use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; use Spatie\LaravelData\Support\TreeNodes\TreeNode; @@ -14,6 +17,8 @@ class TransformationContextFactory { + use ForwardsToPartialsDefinition; + public static function create(): self { return new self(); @@ -23,32 +28,23 @@ public static function create(): self * @param bool $transformValues * @param bool $mapPropertyNames * @param \Spatie\LaravelData\Support\Wrapping\WrapExecutionType $wrapExecutionType - * @param array $includes - * @param array $excludes - * @param array $only - * @param array $except */ protected function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public array $includes = [], - public array $excludes = [], - public array $only = [], - public array $except = [], + public PartialsDefinition $partialsDefinition = new PartialsDefinition(), ) { } - public function get(): TransformationContext - { + public function get( + BaseData|BaseDataCollectable $data + ): TransformationContext { return new TransformationContext( $this->transformValues, $this->mapPropertyNames, $this->wrapExecutionType, - app(PartialsParser::class)->execute($this->includes), - app(PartialsParser::class)->execute($this->excludes), - app(PartialsParser::class)->execute($this->only), - app(PartialsParser::class)->execute($this->except), + PartialTransformationContext::create($data, $this->partialsDefinition), ); } @@ -73,77 +69,8 @@ public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static return $this; } - public function include(string ...$includes): static - { - foreach ($includes as $include) { - $this->includes[$include] = true; - } - - return $this; - } - - public function exclude(string ...$excludes): static - { - foreach ($excludes as $exclude) { - $this->excludes[$exclude] = true; - } - - return $this; - } - - public function only(string ...$only): static - { - foreach ($only as $onlyDefinition) { - $this->only[$onlyDefinition] = true; - } - - return $this; - } - - public function except(string ...$except): static - { - foreach ($except as $exceptDefinition) { - $this->except[$exceptDefinition] = true; - } - - return $this; - } - - public function includeWhen(string $include, bool|Closure $condition): static + protected function getPartialsDefinition(): PartialsDefinition { - $this->includes[$include] = $condition; - - return $this; - } - - public function excludeWhen(string $exclude, bool|Closure $condition): static - { - $this->excludes[$exclude] = $condition; - - return $this; - } - - public function onlyWhen(string $only, bool|Closure $condition): static - { - $this->only[$only] = $condition; - - return $this; - } - - public function exceptWhen(string $except, bool|Closure $condition): static - { - $this->except[$except] = $condition; - - return $this; - } - - public function mergeDataContext(DataContext $context): static - { - $this->includes = array_merge($this->includes, $context->includes); - $this->excludes = array_merge($this->excludes, $context->excludes); - $this->only = array_merge($this->only, $context->only); - $this->except = array_merge($this->includes, $context->except); - - return $this; + return $this->partialsDefinition; } } diff --git a/src/Support/Type.php b/src/Support/Type.php new file mode 100644 index 00000000..9553c72e --- /dev/null +++ b/src/Support/Type.php @@ -0,0 +1,165 @@ +getName() !== 'mixed' && $typeReflection->getName() !== 'null'; + + return new self( + isNullable: $typeReflection->allowsNull(), + isMixed: $typeReflection->getName() === 'mixed', + acceptedTypes: $hasAcceptedTypes + ? [$typeReflection->getName() => self::resolveBaseTypes($typeReflection->getName())] + : [] + ); + } + + if (! $typeReflection instanceof ReflectionUnionType && ! $typeReflection instanceof ReflectionIntersectionType) { + throw new Exception('Cannot create type'); + } + + // TODO: basically rewrite of DataTypefactory, let's rewrite the whole type system + + $isNullable = false; + $isMixed = false; + $acceptedTypes = []; + + foreach ($typeReflection->getTypes() as $subTypeReflection) { + $subType = static::create($subTypeReflection); + + $isNullable = $isNullable || $subType->isNullable; + $isMixed = $isMixed || $subType->isMixed; + $acceptedTypes = [...$acceptedTypes, ...$subType->acceptedTypes]; + } + + return new self( + isNullable: $isNullable, + isMixed: $isMixed, + acceptedTypes: $acceptedTypes + ); + } + + public function isEmpty(): bool + { + return $this->count() === 0; + } + + public function count(): int + { + return count($this->acceptedTypes); + } + + public function acceptsValue(mixed $value): bool + { + if ($this->isNullable && $value === null) { + return true; + } + + $type = gettype($value); + + $type = match ($type) { + 'integer' => 'int', + 'boolean' => 'bool', + 'double' => 'float', + 'object' => $value::class, + default => $type, + }; + + return $this->acceptsType($type); + } + + + public function acceptsType(string $type): bool + { + if ($this->isMixed) { + return true; + } + + if (array_key_exists($type, $this->acceptedTypes)) { + return true; + } + + if (in_array($type, ['string', 'int', 'bool', 'float', 'array'])) { + return false; + } + + $baseTypes = class_exists($type) + ? array_unique([ + ...array_values(class_parents($type)), + ...array_values(class_implements($type)), + ]) + : []; + + foreach ([$type, ...$baseTypes] as $givenType) { + if (array_key_exists($givenType, $this->acceptedTypes)) { + return true; + } + } + + return false; + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + foreach ($this->acceptedTypes as $acceptedType => $acceptedBaseTypes) { + if ($class === $acceptedType) { + return $acceptedType; + } + + if (in_array($class, $acceptedBaseTypes)) { + return $acceptedType; + } + } + + return null; + } + + protected static function resolveBaseTypes(string $type): array + { + if (! class_exists($type)) { + return []; + } + + return array_unique([ + ...array_values(class_parents($type)), + ...array_values(class_implements($type)), + ]); + } +} diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 4aad0b8c..b38efc45 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -15,6 +15,7 @@ use RuntimeException; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\DataCollectableType; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer; @@ -94,7 +95,7 @@ protected function resolveTypeForProperty( DataProperty $dataProperty, MissingSymbolsCollection $missingSymbols, ): ?Type { - if (! $dataProperty->type->isDataCollectable) { + if (! $dataProperty->type->kind->isDataCollectable()) { return $this->reflectionToType( $property, $missingSymbols, @@ -102,11 +103,11 @@ protected function resolveTypeForProperty( ); } - $collectionType = match ($dataProperty->type->dataCollectableType) { - DataCollectableType::Default => $this->defaultCollectionType($dataProperty->type->dataClass), - DataCollectableType::Paginated => $this->paginatedCollectionType($dataProperty->type->dataClass), - DataCollectableType::CursorPaginated => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), - null => throw new RuntimeException('Cannot end up here since the type is dataCollectable') + $collectionType = match ($dataProperty->type->kind) { + DataTypeKind::Enumerable, DataTypeKind::Array, DataTypeKind::DataCollection => $this->defaultCollectionType($dataProperty->type->dataClass), + DataTypeKind::Paginator, DataTypeKind::DataPaginatedCollection => $this->paginatedCollectionType($dataProperty->type->dataClass), + DataTypeKind::CursorPaginator, DataTypeKind::DataCursorPaginatedCollection => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), + default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') }; if ($dataProperty->type->isNullable) { diff --git a/src/Support/Validation/References/RouteParameterReference.php b/src/Support/Validation/References/RouteParameterReference.php index 5aa80b30..81780e7b 100644 --- a/src/Support/Validation/References/RouteParameterReference.php +++ b/src/Support/Validation/References/RouteParameterReference.php @@ -10,17 +10,22 @@ class RouteParameterReference implements Stringable public function __construct( public readonly string $routeParameter, public readonly ?string $property = null, + public readonly bool $nullable = false, ) { } - public function getValue(): string + public function getValue(): ?string { $parameter = \request()->route($this->routeParameter); - if ($parameter === null) { + if ($parameter === null && $this->nullable === false) { throw CannotResolveRouteParameterReference::parameterNotFound($this->routeParameter, $this->property); } + if ($parameter === null) { + return null; + } + if ($this->property === null) { return $parameter; } diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 2e95a553..ec8b9980 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -507,10 +507,6 @@ function (string $operation, array $arguments, array $expected) { $invaded = invade($unserialized); - expect($invaded->_partialTrees)->toBeNull(); - expect($invaded->_includes)->toBeEmpty(); - expect($invaded->_excludes)->toBeEmpty(); - expect($invaded->_only)->toBeEmpty(); - expect($invaded->_except)->toBeEmpty(); + expect($invaded->_dataContext)->toBeNull(); expect($invaded->_wrap)->toBeNull(); }); diff --git a/tests/DataTest.php b/tests/DataTest.php index 9bb76734..d0358ffa 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -607,7 +607,7 @@ public function __construct( } }; - expect($data)->transform2(TransformationContextFactory::create()->mapPropertyNames(false)) + expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) ->toMatchArray([ 'camelName' => 'Freek', ]); @@ -622,7 +622,7 @@ public function __construct( } }; - expect($data)->transform2(TransformationContextFactory::create()->mapPropertyNames(false)) + expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) ->toMatchArray([ 'camelName' => 'Freek', ]); @@ -636,7 +636,7 @@ public function __construct( ) { } }; - expect($data->transform2())->toMatchArray([ + expect($data->transform())->toMatchArray([ 'snake_name' => 'Freek', ]); }); @@ -2201,10 +2201,39 @@ public function __construct( $invaded = invade($unserialized); - expect($invaded->_partialTrees)->toBeNull(); - expect($invaded->_includes)->toBeEmpty(); - expect($invaded->_excludes)->toBeEmpty(); - expect($invaded->_only)->toBeEmpty(); - expect($invaded->_except)->toBeEmpty(); + expect($invaded->_dataContext)->toBeNull(); expect($invaded->_wrap)->toBeNull(); }); + + +it('can use an array to store data', function () { + $dataClass = new class( + [LazyData::from('A'), LazyData::from('B')], + collect([LazyData::from('A'), LazyData::from('B')]), + ) extends Data + { + public function __construct( + #[DataCollectionOf(SimpleData::class)] + public array $array, + #[DataCollectionOf(SimpleData::class)] + public Collection $collection, + ) { + } + }; + + $d = $dataClass::from([ + 'array' => ['A', 'B'], + 'collection' => ['A', 'B'], + ]); + + expect($dataClass->include('array.name', 'collection.name')->toArray())->toBe([ + 'array' => [ + ['name' => 'A'], + ['name' => 'B'], + ], + 'collection' => [ + ['name' => 'A'], + ['name' => 'B'], + ], + ]); +})->skip('TODO: yet to impelment'); diff --git a/tests/Fakes/CollectionAnnotationsData.php b/tests/Fakes/CollectionAnnotationsData.php index c7b82be2..1497f5e4 100644 --- a/tests/Fakes/CollectionAnnotationsData.php +++ b/tests/Fakes/CollectionAnnotationsData.php @@ -2,52 +2,71 @@ namespace Spatie\LaravelData\Tests\Fakes; +use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\DataCollection; +/** + * @property DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $propertyO + * @property \Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyP + * @property DataCollection $propertyQ + * @property array<\Spatie\LaravelData\Tests\Fakes\SimpleData> $propertyR + * @property \Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyS + * @property array $propertyT + */ class CollectionAnnotationsData { - /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[]|\Spatie\LaravelData\DataCollection */ - public DataCollection $propertyA; - - /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[]|DataCollection */ - public DataCollection $propertyB; - - /** @var \Spatie\LaravelData\DataCollection|\Spatie\LaravelData\Tests\Fakes\SimpleData[] */ - public DataCollection $propertyC; - - /** @var DataCollection|\Spatie\LaravelData\Tests\Fakes\SimpleData[] */ - public DataCollection $propertyD; - /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[] */ - public DataCollection $propertyE; + public array $propertyA; /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[]|null */ - public DataCollection $propertyF; + public ?array $propertyB; /** @var null|\Spatie\LaravelData\Tests\Fakes\SimpleData[] */ - public DataCollection $propertyG; + public ?array $propertyC; - /** @var ?Spatie\LaravelData\Tests\Fakes\SimpleData[] */ - public DataCollection $propertyH; + /** @var ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] */ + public array $propertyD; /** @var \Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ - public DataCollection $propertyI; + public DataCollection $propertyE; - /** @var ?Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ - public DataCollection $propertyJ; + /** @var ?\Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + public ?DataCollection $propertyF; /** @var SimpleData[] */ - public DataCollection $propertyK; + public array $propertyG; #[DataCollectionOf(SimpleData::class)] - public DataCollection $propertyL; + public DataCollection $propertyH; /** @var SimpleData */ - public DataCollection $propertyM; + public DataCollection $propertyI; // FAIL /** @var \Spatie\LaravelData\Lazy[] */ - public DataCollection $propertyN; + public array $propertyJ; // Fail + + public DataCollection $propertyK; + + /** @var array<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + public array $propertyL; + + /** @var LengthAwarePaginator<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + public LengthAwarePaginator $propertyM; + + /** @var \Illuminate\Support\Collection<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + public Collection $propertyN; public DataCollection $propertyO; + + public DataCollection $propertyP; + + public DataCollection $propertyQ; + + public array $propertyR; + + public array $propertyS; + public array $propertyT; } diff --git a/tests/Resolvers/EmptyDataResolverTest.php b/tests/Resolvers/EmptyDataResolverTest.php index c9366660..722dfcf9 100644 --- a/tests/Resolvers/EmptyDataResolverTest.php +++ b/tests/Resolvers/EmptyDataResolverTest.php @@ -79,9 +79,9 @@ function assertEmptyPropertyValue( }); it('will return the base type for lazy types', function () { - // $this->assertEmptyPropertyValue(null, new class() { - // public Lazy | string $property; - // }); + assertEmptyPropertyValue(null, new class() { + public Lazy|string $property; + }); assertEmptyPropertyValue([], new class () { public Lazy|array $property; diff --git a/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php b/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php new file mode 100644 index 00000000..84e82610 --- /dev/null +++ b/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php @@ -0,0 +1,100 @@ +getForProperty(new ReflectionProperty(CollectionAnnotationsData::class, $property)); + + expect($annotations)->toEqual($expected); + } +)->with(function () { + yield 'propertyA' => [ + 'property' => 'propertyA', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyB' => [ + 'property' => 'propertyB', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyC' => [ + 'property' => 'propertyC', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyD' => [ + 'property' => 'propertyD', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyE' => [ + 'property' => 'propertyE', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyF' => [ + 'property' => 'propertyF', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyG' => [ + 'property' => 'propertyG', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyH' => [ + 'property' => 'propertyH', + 'expected' => null, // Attribute + ]; + + yield 'propertyI' => [ + 'property' => 'propertyI', + 'expected' => null, // Invalid definition + ]; + + yield 'propertyJ' => [ + 'property' => 'propertyJ', + 'expected' => null,// Invalid definition + ]; + + yield 'propertyK' => [ + 'property' => 'propertyK', + 'expected' => null, // No definition + ]; + + yield 'propertyL' => [ + 'property' => 'propertyL', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyM' => [ + 'property' => 'propertyM', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; + + yield 'propertyN' => [ + 'property' => 'propertyN', + 'expected' => new DataCollectableAnnotation(SimpleData::class), + ]; +}); + +it('can get the data class for a data collection by class annotation', function () { + $annotations = app(DataCollectableAnnotationReader::class)->getForClass(new ReflectionClass(CollectionAnnotationsData::class)); + + expect($annotations)->toEqualCanonicalizing([ + new DataCollectableAnnotation(SimpleData::class, property: 'propertyO'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyP'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyQ'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyR'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyS'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyT'), + ]); +}); diff --git a/tests/Support/DataClassTest.php b/tests/Support/DataClassTest.php index a9718785..2edb1f5c 100644 --- a/tests/Support/DataClassTest.php +++ b/tests/Support/DataClassTest.php @@ -68,45 +68,45 @@ public function __construct( }); it('wont throw an error if a non existing attribute is used on a data class', function () { - expect(PhpStormClassAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') - ->and(NonExistingAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') - ->and(PhpStormClassAttributeData::from((object)['property' => 'hello'])->property)->toEqual('hello') - ->and(PhpStormClassAttributeData::from('{"property": "hello"}')->property)->toEqual('hello') - ->and(ModelWithPhpStormAttributeData::from((new DummyModel())->fill(['id' => 1]))->id)->toEqual(1); -}); - -#[\JetBrains\PhpStorm\Immutable] -class PhpStormClassAttributeData extends Data -{ - public readonly string $property; - - public function __construct(string $property) + #[\JetBrains\PhpStorm\Immutable] + class PhpStormClassAttributeData extends Data { - $this->property = $property; - } -} + public readonly string $property; -#[\Foo\Bar] -class NonExistingAttributeData extends Data -{ - public readonly string $property; + public function __construct(string $property) + { + $this->property = $property; + } + } - public function __construct(string $property) + #[\Foo\Bar] + class NonExistingAttributeData extends Data { - $this->property = $property; - } -} - -#[\JetBrains\PhpStorm\Immutable] -class ModelWithPhpStormAttributeData extends Data -{ - public function __construct( - public int $id - ) { + public readonly string $property; + + public function __construct(string $property) + { + $this->property = $property; + } } - public static function fromDummyModel(DummyModel $model) + #[\JetBrains\PhpStorm\Immutable] + class ModelWithPhpStormAttributeData extends Data { - return new self($model->id); + public function __construct( + public int $id + ) { + } + + public static function fromDummyModel(DummyModel $model) + { + return new self($model->id); + } } -} + + expect(PhpStormClassAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') + ->and(NonExistingAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') + ->and(PhpStormClassAttributeData::from((object)['property' => 'hello'])->property)->toEqual('hello') + ->and(PhpStormClassAttributeData::from('{"property": "hello"}')->property)->toEqual('hello') + ->and(ModelWithPhpStormAttributeData::from((new DummyModel())->fill(['id' => 1]))->id)->toEqual(1); +}); diff --git a/tests/Support/DataMethodTest.php b/tests/Support/DataMethodTest.php index 20b5574a..d7ee0b2f 100644 --- a/tests/Support/DataMethodTest.php +++ b/tests/Support/DataMethodTest.php @@ -1,9 +1,15 @@ parameters->toHaveCount(2) ->isPublic->toBeTrue() ->isStatic->toBeFalse() - ->isCustomCreationMethod->toBeFalse() + ->customCreationMethodType->toBe(CustomCreationMethodType::None) ->and($method->parameters[0])->toBeInstanceOf(DataProperty::class) ->and($method->parameters[1])->toBeInstanceOf(DataParameter::class); }); @@ -35,7 +41,7 @@ public function __construct( $class = new class () extends Data { public static function fromString( string $property, - ) { + ): self { } }; @@ -46,10 +52,60 @@ public static function fromString( ->parameters->toHaveCount(1) ->isPublic->toBeTrue() ->isStatic->toBeTrue() - ->isCustomCreationMethod->toBeTrue() + ->customCreationMethodType->toBe(CustomCreationMethodType::Object) ->and($method->parameters[0])->toBeInstanceOf(DataParameter::class); }); +it('can create a data method from a magic collect method', function () { + $class = new class () extends Data { + public static function collectArray( + array $items, + ): array { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + + expect($method) + ->name->toEqual('collectArray') + ->parameters->toHaveCount(1) + ->isPublic->toBeTrue() + ->isStatic->toBeTrue() + ->customCreationMethodType->toBe(CustomCreationMethodType::Collection) + ->returnType->toEqual(new Type(isNullable: false, isMixed: false, acceptedTypes: ['array' => []])) + ->and($method->parameters[0])->toBeInstanceOf(DataParameter::class); +}); + +it('can create a data method from a magic collect method with nullable return type', function () { + $class = new class () extends Data { + public static function collectArray( + array $items, + ): ?array { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + + expect($method) + ->customCreationMethodType->toBe(CustomCreationMethodType::Collection) + ->returnType->toEqual(new Type(isNullable: true, isMixed: false, acceptedTypes: ['array' => []])); +}); + +it('will not create a magical collection method when no return type specified', function () { + $class = new class () extends Data { + public static function collectArray( + array $items, + ) { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + + expect($method) + ->customCreationMethodType->toBe(CustomCreationMethodType::None) + ->returnType->toEqual(new Type(isNullable: true, isMixed: true, acceptedTypes: [])); +}); + it('correctly accepts single values as magic creation method', function () { $class = new class () extends Data { public static function fromString( @@ -137,3 +193,56 @@ public static function fromString( ->accepts()->toBeFalse() ->accepts('Hello', 'World', 'Nope')->toBeFalse(); }); + +it('can check if a magical method can return the exact type', function () { + $class = new class () extends Data { + public static function collectCollection( + Collection $property, + ): Collection { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + + expect($method->returns(Collection::class))->toBeTrue(); +}); + +it('can check if a magical method can return the sub type', function () { + $class = new class () extends Data { + public static function collectCollection( + Collection $property, + ): Collection { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + + expect($method->returns(EloquentCollection::class))->toBeTrue(); +}); + +it('can check if a magical method can return a built in type', function () { + $class = new class () extends Data { + public static function collectCollectionToArray( + Collection $property, + ): array { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectCollectionToArray')); + + expect($method->returns('array'))->toBeTrue(); +}); + + +it('can check if a magical method cannot return a parent type', function () { + $class = new class () extends Data { + public static function collectCollection( + Collection $property, + ): Collection { + } + }; + + $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + + expect($method->returns(Enumerable::class))->toBeFalse(); +}); diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index 3d217a13..a9dc34a0 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -3,6 +3,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\Factories\DataTypeFactory; it('can create a data parameter', function () { $class = new class ('', '', '') extends Data { @@ -23,7 +24,7 @@ public function __construct( ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(DataType::create($reflection)); + ->type->toEqual(DataTypeFactory::create()->build($reflection)); $reflection = new ReflectionParameter([$class::class, '__construct'], 'withoutType'); $parameter = DataParameter::create($reflection); @@ -33,7 +34,7 @@ public function __construct( ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(DataType::create($reflection)); + ->type->toEqual(DataTypeFactory::create()->build($reflection)); $reflection = new ReflectionParameter([$class::class, '__construct'], 'property'); $parameter = DataParameter::create($reflection); @@ -43,7 +44,7 @@ public function __construct( ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(DataType::create($reflection)); + ->type->toEqual(DataTypeFactory::create()->build($reflection)); $reflection = new ReflectionParameter([$class::class, '__construct'], 'propertyWithDefault'); $parameter = DataParameter::create($reflection); @@ -53,5 +54,5 @@ public function __construct( ->isPromoted->toBeTrue() ->hasDefaultValue->toBeTrue() ->defaultValue->toEqual('hello') - ->type->toEqual(DataType::create($reflection)); + ->type->toEqual(DataTypeFactory::create()->build($reflection)); }); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index ac6eb934..27c67dc0 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -5,10 +5,14 @@ use Illuminate\Contracts\Support\Jsonable; use Illuminate\Contracts\Support\Responsable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Pagination\CursorPaginator; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\PrepareableData; use Spatie\LaravelData\Contracts\ResponsableData; @@ -19,12 +23,15 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataCollectableType; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotFindDataClass; use Spatie\LaravelData\Exceptions\InvalidDataType; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\Factories\DataTypeFactory; use Spatie\LaravelData\Tests\Fakes\CollectionAnnotationsData; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; @@ -33,7 +40,12 @@ function resolveDataType(object $class, string $property = 'property'): DataType { - return DataType::create(new ReflectionProperty($class, $property)); + $reflectionProperty = new ReflectionProperty($class, $property); + + return DataTypeFactory::create()->build( + $reflectionProperty, + DataCollectableAnnotationReader::create()->getForClass($reflectionProperty->getDeclaringClass())[$property] ?? null, + ); } it('can deduce a type without definition', function () { @@ -46,9 +58,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeTrue() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toBeEmpty(); }); @@ -62,9 +74,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys(['string']); }); @@ -78,9 +90,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectionClass->toBeNull() ->acceptedTypes->toHaveKeys(['string']); }); @@ -94,10 +106,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys(['string', 'int']); }); @@ -111,10 +122,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys(['string', 'int']); }); @@ -128,10 +138,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys([ DateTime::class, DateTimeImmutable::class, @@ -148,10 +157,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeTrue() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys([]); }); @@ -165,10 +173,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys(['string']); }); @@ -182,10 +189,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeTrue() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() + ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys(['string']); }); @@ -205,10 +211,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeTrue() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataObject) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys([SimpleData::class]); }); @@ -222,10 +227,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() - ->isDataObject->toBeTrue() - ->isDataCollectable->toBeFalse() - ->dataCollectableType->toBeNull() - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataObject) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBeNull() ->acceptedTypes->toHaveKeys([SimpleData::class]); }); @@ -240,10 +244,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeTrue() - ->dataCollectableType->toEqual(DataCollectableType::Default) - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class) ->acceptedTypes->toHaveKeys([DataCollection::class]); }); @@ -258,10 +261,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeTrue() - ->dataCollectableType->toEqual(DataCollectableType::Default) - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class) ->acceptedTypes->toHaveKeys([DataCollection::class]); }); @@ -276,10 +278,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeTrue() - ->dataCollectableType->toEqual(DataCollectableType::Paginated) - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(PaginatedDataCollection::class) ->acceptedTypes->toHaveKeys([PaginatedDataCollection::class]); }); @@ -294,10 +295,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeTrue() - ->dataCollectableType->toEqual(DataCollectableType::Paginated) - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(PaginatedDataCollection::class) ->acceptedTypes->toHaveKeys([PaginatedDataCollection::class]); }); @@ -312,10 +312,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeTrue() - ->dataCollectableType->toEqual(DataCollectableType::CursorPaginated) - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) ->acceptedTypes->toHaveKeys([CursorPaginatedDataCollection::class]); }); @@ -330,13 +329,148 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() - ->isDataObject->toBeFalse() - ->isDataCollectable->toBeTrue() - ->dataCollectableType->toEqual(DataCollectableType::CursorPaginated) - ->dataClass->toEqual(SimpleData::class) + ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) ->acceptedTypes->toHaveKeys([CursorPaginatedDataCollection::class]); }); +it('can deduce an array data collection type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public array $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeFalse() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array') + ->acceptedTypes->toHaveKeys(['array']); +}); + +it('can deduce an array data collection union type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public array|Lazy $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeTrue() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array') + ->acceptedTypes->toHaveKeys(['array']); +}); + +it('can deduce an enumerable data collection type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public Collection $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeFalse() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::Enumerable) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(Collection::class) + ->acceptedTypes->toHaveKeys([Collection::class]); +}); + +it('can deduce an enumerable data collection union type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public Collection|Lazy $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeTrue() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::Enumerable) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(Collection::class) + ->acceptedTypes->toHaveKeys([Collection::class]); +}); + +it('can deduce a paginator data collection type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public LengthAwarePaginator $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeFalse() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::Paginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(LengthAwarePaginator::class) + ->acceptedTypes->toHaveKeys([LengthAwarePaginator::class]); +}); + +it('can deduce a paginator data collection union type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public LengthAwarePaginator|Lazy $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeTrue() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::Paginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(LengthAwarePaginator::class) + ->acceptedTypes->toHaveKeys([LengthAwarePaginator::class]); +}); + +it('can deduce a cursor paginator data collection type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public CursorPaginator $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeFalse() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::CursorPaginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginator::class) + ->acceptedTypes->toHaveKeys([CursorPaginator::class]); +}); + +it('can deduce a cursor paginator data collection union type', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public CursorPaginator|Lazy $property; + }); + + expect($type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->isLazy->toBeTrue() + ->isOptional->toBeFalse() + ->kind->toBe(DataTypeKind::CursorPaginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginator::class) + ->acceptedTypes->toHaveKeys([CursorPaginator::class]); +}); + it('cannot have multiple data types', function () { resolveDataType(new class () { public SimpleData|ComplicatedData $property; @@ -362,28 +496,28 @@ function (object $class, array $expected) { expect(resolveDataType($class)->acceptedTypes)->toEqualCanonicalizing($expected); } )->with(function () { - yield [ + yield 'no type' => [ 'class' => new class () { public $property; }, 'expected' => [], ]; - yield [ + yield 'mixed' => [ 'class' => new class () { public mixed $property; }, 'expected' => [], ]; - yield [ + yield 'single' => [ 'class' => new class () { public string $property; }, 'expected' => ['string' => []], ]; - yield [ + yield 'multi' => [ 'class' => new class () { public string|int|bool|float|array $property; }, @@ -396,7 +530,7 @@ function (object $class, array $expected) { ], ]; - yield [ + yield 'data' => [ 'class' => new class () { public SimpleData $property; }, @@ -410,18 +544,18 @@ function (object $class, array $expected) { Arrayable::class, DataObject::class, AppendableData::class, - PrepareableData::class, BaseData::class, IncludeableData::class, ResponsableData::class, TransformableData::class, ValidateableData::class, WrappableData::class, + EmptyData::class, ], ], ]; - yield [ + yield 'enum' => [ 'class' => new class () { public DummyBackedEnum $property; }, @@ -698,92 +832,45 @@ function (object $class, string $type, ?string $expectedType) { ]; }); -it( - 'can get the data class for a data collection by annotation', - function (string $property, ?string $expected) { - $dataType = DataType::create(new ReflectionProperty(CollectionAnnotationsData::class, $property)); - - expect($dataType->dataClass)->toEqual($expected); - } -)->with(function () { - yield [ - 'property' => 'propertyA', - 'expected' => SimpleData::class, - ]; - - yield [ - 'property' => 'propertyB', - 'expected' => SimpleData::class, - ]; - - yield [ - 'property' => 'propertyC', - 'expected' => SimpleData::class, - ]; - - yield [ - 'property' => 'propertyD', - 'expected' => SimpleData::class, - ]; - - yield [ - 'property' => 'propertyE', - 'expected' => SimpleData::class, - ]; - - yield [ - 'property' => 'propertyF', - 'expected' => SimpleData::class, - ]; +it('can annotate data collections using attributes', function () { + $type = resolveDataType(new class () { + #[DataCollectionOf(SimpleData::class)] + public DataCollection $property; + }); - yield [ - 'property' => 'propertyG', - 'expected' => SimpleData::class, - ]; + expect($type) + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); +}); - yield [ - 'property' => 'propertyH', - 'expected' => SimpleData::class, - ]; +it('can annotate data collections using var annotations', function () { + $type = resolveDataType(new class () { + /** @var DataCollection */ + public DataCollection $property; + }); - yield [ - 'property' => 'propertyI', - 'expected' => SimpleData::class, - ]; + expect($type) + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); +}); - yield [ - 'property' => 'propertyJ', - 'expected' => SimpleData::class, - ]; +it('can annotate data collections using property annotations', function () { + /** + * @property DataCollection $property + */ + class TestDataTypeWithClassAnnotatedProperty{ + public function __construct( + public DataCollection $property, + ) { + } + } - yield [ - 'property' => 'propertyK', - 'expected' => SimpleData::class, - ]; + $type = resolveDataType(new \TestDataTypeWithClassAnnotatedProperty(SimpleData::collection([]))); - yield [ - 'property' => 'propertyL', - 'expected' => SimpleData::class, - ]; -}); - -it('cannot get the data class for invalid annotations') - ->tap( - fn (string $property) => DataType::create( - new ReflectionProperty(CollectionAnnotationsData::class, $property) - ) - ) - ->throws(CannotFindDataClass::class) - ->with(function () { - yield [ - 'property' => 'propertyM', - ]; - - yield [ - 'property' => 'propertyN', - ]; - - yield [ - 'property' => 'propertyO', - ]; - }); + expect($type) + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); +}); From 30b03b66cc4fe8929f6e72678a6374a19cec39c7 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 21 Feb 2023 17:18:25 +0100 Subject: [PATCH 005/124] wip --- src/Casts/DateTimeInterfaceCast.php | 2 +- src/Casts/EnumCast.php | 2 +- src/DataPipes/CastPropertiesDataPipe.php | 2 +- src/DataPipes/DefaultValuesDataPipe.php | 2 +- .../DataPropertyCanOnlyHaveOneType.php | 2 +- src/Resolvers/DataValidationRulesResolver.php | 2 +- src/Resolvers/EmptyDataResolver.php | 9 +- .../BuiltInTypesRuleInferrer.php | 12 +- src/RuleInferrers/NullableRuleInferrer.php | 2 +- src/RuleInferrers/RequiredRuleInferrer.php | 2 +- src/Support/DataConfig.php | 2 +- src/Support/DataMethod.php | 16 +- src/Support/DataType.php | 39 +-- src/Support/Factories/DataTypeFactory.php | 124 ++++----- src/Support/Type.php | 165 ----------- .../DataTypeScriptTransformer.php | 2 +- src/Support/Types/IntersectionType.php | 35 +++ src/Support/Types/MultiType.php | 67 +++++ src/Support/Types/PartialType.php | 116 ++++++++ src/Support/Types/SingleType.php | 58 ++++ src/Support/Types/Type.php | 59 ++++ src/Support/Types/UndefinedType.php | 26 ++ src/Support/Types/UnionType.php | 34 +++ tests/Resolvers/EmptyDataResolverTest.php | 14 +- tests/Support/DataMethodTest.php | 22 +- tests/Support/DataTypeTest.php | 260 +++++++++++------- 26 files changed, 666 insertions(+), 410 deletions(-) delete mode 100644 src/Support/Type.php create mode 100644 src/Support/Types/IntersectionType.php create mode 100644 src/Support/Types/MultiType.php create mode 100644 src/Support/Types/PartialType.php create mode 100644 src/Support/Types/SingleType.php create mode 100644 src/Support/Types/Type.php create mode 100644 src/Support/Types/UndefinedType.php create mode 100644 src/Support/Types/UnionType.php diff --git a/src/Casts/DateTimeInterfaceCast.php b/src/Casts/DateTimeInterfaceCast.php index 26584da6..575308e4 100644 --- a/src/Casts/DateTimeInterfaceCast.php +++ b/src/Casts/DateTimeInterfaceCast.php @@ -21,7 +21,7 @@ public function cast(DataProperty $property, mixed $value, array $context): Date { $formats = collect($this->format ?? config('data.date_format')); - $type = $this->type ?? $property->type->findAcceptedTypeForBaseType(DateTimeInterface::class); + $type = $this->type ?? $property->type->type->findAcceptedTypeForBaseType(DateTimeInterface::class); if ($type === null) { return Uncastable::create(); diff --git a/src/Casts/EnumCast.php b/src/Casts/EnumCast.php index e5d9a7f5..bd0c1b64 100644 --- a/src/Casts/EnumCast.php +++ b/src/Casts/EnumCast.php @@ -16,7 +16,7 @@ public function __construct( public function cast(DataProperty $property, mixed $value, array $context): BackedEnum | Uncastable { - $type = $this->type ?? $property->type->findAcceptedTypeForBaseType(BackedEnum::class); + $type = $this->type ?? $property->type->type->findAcceptedTypeForBaseType(BackedEnum::class); if ($type === null) { return Uncastable::create(); diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 0e84180a..7ca6ce9f 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -74,7 +74,7 @@ protected function cast( protected function shouldBeCasted(DataProperty $property, mixed $value): bool { return gettype($value) === 'object' - ? ! $property->type->acceptsValue($value) + ? ! $property->type->type->acceptsValue($value) : true; } } diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index bb009787..c3aa733e 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -27,7 +27,7 @@ public function handle(mixed $payload, DataClass $class, Collection $properties) return; } - if ($property->type->isNullable) { + if ($property->type->isNullable()) { $properties[$property->name] = null; return; diff --git a/src/Exceptions/DataPropertyCanOnlyHaveOneType.php b/src/Exceptions/DataPropertyCanOnlyHaveOneType.php index 4d4d6fa7..9c6ea9ae 100644 --- a/src/Exceptions/DataPropertyCanOnlyHaveOneType.php +++ b/src/Exceptions/DataPropertyCanOnlyHaveOneType.php @@ -9,6 +9,6 @@ class DataPropertyCanOnlyHaveOneType extends Exception { public static function create(DataProperty $property) { - return new self("When resolving an empty data property, it can only have one type, {$property->className}::{$property->name} has {$property->type->count()} types. You can overwrite this by providing an empty value for the property in the `empty` call."); + return new self("When resolving an empty data property, it can only have one type, {$property->className}::{$property->name} has multiple types. You can overwrite this by providing an empty value for the property in the `empty` call."); } } diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index 003edc3f..2cf1aee6 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -80,7 +80,7 @@ protected function resolveDataSpecificRules( return; } - if ($dataProperty->type->isNullable && Arr::get($fullPayload, $propertyPath->get()) === null) { + if ($dataProperty->type->isNullable() && Arr::get($fullPayload, $propertyPath->get()) === null) { return; } diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index d1498433..0190699f 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -5,6 +5,7 @@ use Spatie\LaravelData\Exceptions\DataPropertyCanOnlyHaveOneType; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Types\MultiType; use Traversable; class EmptyDataResolver @@ -32,15 +33,15 @@ public function execute(string $class, array $extra = []): array protected function getValueForProperty(DataProperty $property): mixed { - if ($property->type->isMixed) { + if ($property->type->isMixed()) { return null; } - if ($property->type->count() > 1) { + if ($property->type->type instanceof MultiType && $property->type->type->acceptedTypesCount() > 1) { throw DataPropertyCanOnlyHaveOneType::create($property); } - if ($property->type->acceptsType('array')) { + if ($property->type->type->acceptsType('array')) { return []; } @@ -52,7 +53,7 @@ protected function getValueForProperty(DataProperty $property): mixed return []; } - if ($property->type->findAcceptedTypeForBaseType(Traversable::class) !== null) { + if ($property->type->type->findAcceptedTypeForBaseType(Traversable::class) !== null) { return []; } diff --git a/src/RuleInferrers/BuiltInTypesRuleInferrer.php b/src/RuleInferrers/BuiltInTypesRuleInferrer.php index 4a863c3f..ffbb3ac4 100644 --- a/src/RuleInferrers/BuiltInTypesRuleInferrer.php +++ b/src/RuleInferrers/BuiltInTypesRuleInferrer.php @@ -20,29 +20,29 @@ public function handle( PropertyRules $rules, ValidationContext $context, ): PropertyRules { - if ($property->type->acceptsType('int')) { + if ($property->type->type->acceptsType('int')) { $rules->add(new Numeric()); } - if ($property->type->acceptsType('string')) { + if ($property->type->type->acceptsType('string')) { $rules->add(new StringType()); } - if ($property->type->acceptsType('bool')) { + if ($property->type->type->acceptsType('bool')) { $rules->removeType(RequiringRule::class); $rules->add(new BooleanType()); } - if ($property->type->acceptsType('float')) { + if ($property->type->type->acceptsType('float')) { $rules->add(new Numeric()); } - if ($property->type->acceptsType('array')) { + if ($property->type->type->acceptsType('array')) { $rules->add(new ArrayType()); } - if ($enumClass = $property->type->findAcceptedTypeForBaseType(BackedEnum::class)) { + if ($enumClass = $property->type->type->findAcceptedTypeForBaseType(BackedEnum::class)) { $rules->add(new Enum($enumClass)); } diff --git a/src/RuleInferrers/NullableRuleInferrer.php b/src/RuleInferrers/NullableRuleInferrer.php index 5946c51d..0eba9fd8 100644 --- a/src/RuleInferrers/NullableRuleInferrer.php +++ b/src/RuleInferrers/NullableRuleInferrer.php @@ -14,7 +14,7 @@ public function handle( PropertyRules $rules, ValidationContext $context, ): PropertyRules { - if ($property->type->isNullable && ! $rules->hasType(Nullable::class)) { + if ($property->type->isNullable() && ! $rules->hasType(Nullable::class)) { $rules->prepend(new Nullable()); } diff --git a/src/RuleInferrers/RequiredRuleInferrer.php b/src/RuleInferrers/RequiredRuleInferrer.php index 19c4f8d7..dfd72bf9 100644 --- a/src/RuleInferrers/RequiredRuleInferrer.php +++ b/src/RuleInferrers/RequiredRuleInferrer.php @@ -27,7 +27,7 @@ public function handle( protected function shouldAddRule(DataProperty $property, PropertyRules $rules): bool { - if ($property->type->isNullable || $property->type->isOptional) { + if ($property->type->isNullable() || $property->type->isOptional) { return false; } diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index e35e8219..6fe5a9a5 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -47,7 +47,7 @@ public function getDataClass(string $class): DataClass public function findGlobalCastForProperty(DataProperty $property): ?Cast { - foreach ($property->type->acceptedTypes as $acceptedType => $baseTypes) { + foreach ($property->type->type->getAcceptedTypes() as $acceptedType => $baseTypes) { foreach ([$acceptedType, ...$baseTypes] as $type) { if ($cast = $this->casts[$type] ?? null) { return $cast; diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 1f3fe2ec..f7b4b25d 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -9,6 +9,7 @@ use ReflectionParameter; use ReflectionUnionType; use Spatie\LaravelData\Enums\CustomCreationMethodType; +use Spatie\LaravelData\Support\Types\UndefinedType; /** * @property Collection $parameters @@ -21,13 +22,16 @@ public function __construct( public readonly bool $isStatic, public readonly bool $isPublic, public readonly CustomCreationMethodType $customCreationMethodType, - public readonly ?Type $returnType, + public readonly \Spatie\LaravelData\Support\Types\Type $returnType, ) { } public static function create(ReflectionMethod $method): self { - $returnType = Type::create($method->getReturnType()); + $returnType = \Spatie\LaravelData\Support\Types\Type::forReflection( + $method->getReturnType(), + $method->class, + ); return new self( $method->name, @@ -61,13 +65,13 @@ public static function createConstructor(?ReflectionMethod $method, Collection $ false, $method->isPublic(), CustomCreationMethodType::None, - null, + new UndefinedType(), ); } protected static function resolveCustomCreationMethodType( ReflectionMethod $method, - ?Type $returnType, + ?\Spatie\LaravelData\Support\Types\Type $returnType, ): CustomCreationMethodType { if (! $method->isStatic() || ! $method->isPublic() @@ -82,7 +86,7 @@ protected static function resolveCustomCreationMethodType( return CustomCreationMethodType::Object; } - if (str_starts_with($method->name, 'collect') && $returnType && count($returnType) > 0) { + if (str_starts_with($method->name, 'collect') && ! $returnType instanceof UndefinedType) { return CustomCreationMethodType::Collection; } @@ -111,7 +115,7 @@ public function accepts(mixed ...$input): bool continue; } - if (! $parameter->type->acceptsValue($input[$index])) { + if (! $parameter->type->type->acceptsValue($input[$index])) { return false; } } diff --git a/src/Support/DataType.php b/src/Support/DataType.php index cbc4cb24..1fab0a09 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -2,46 +2,35 @@ namespace Spatie\LaravelData\Support; -use Countable; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Pagination\AbstractPaginator; -use Illuminate\Support\Arr; -use Illuminate\Support\Collection; -use IteratorAggregate; use ReflectionIntersectionType; use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; +use ReflectionType; use ReflectionUnionType; -use Spatie\LaravelData\Attributes\DataCollectionOf; -use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\CursorPaginatedDataCollection; -use Spatie\LaravelData\DataCollection; -use Spatie\LaravelData\Enums\DataCollectableType; use Spatie\LaravelData\Enums\DataTypeKind; -use Spatie\LaravelData\Exceptions\CannotFindDataClass; -use Spatie\LaravelData\Exceptions\InvalidDataType; -use Spatie\LaravelData\Lazy; -use Spatie\LaravelData\Optional; -use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Types\Type; use TypeError; -use function Pest\Laravel\instance; -/** - * @property class-string|null $dataClass - */ -class DataType extends Type +class DataType { public function __construct( - bool $isNullable, - bool $isMixed, + public readonly Type $type, public readonly bool $isLazy, public readonly bool $isOptional, public readonly DataTypeKind $kind, public readonly ?string $dataClass, public readonly ?string $dataCollectableClass, - array $acceptedTypes, ) { - parent::__construct($isNullable, $isMixed, $acceptedTypes); + } + + public function isNullable(): bool + { + return $this->type->isNullable; + } + + public function isMixed(): bool + { + return $this->type->isMixed; } } diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index 69e91720..d92425cf 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -28,6 +28,12 @@ use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Type; +use Spatie\LaravelData\Support\Types\IntersectionType; +use Spatie\LaravelData\Support\Types\MultiType; +use Spatie\LaravelData\Support\Types\PartialType; +use Spatie\LaravelData\Support\Types\SingleType; +use Spatie\LaravelData\Support\Types\UndefinedType; +use Spatie\LaravelData\Support\Types\UnionType; use TypeError; use function Pest\Laravel\instance; @@ -44,16 +50,23 @@ public function build( ): DataType { $type = $property->getType(); + $class = match ($property::class){ + ReflectionParameter::class => $property->getDeclaringClass()?->name, + ReflectionProperty::class => $property->class, + }; + return match (true) { $type === null => $this->buildForEmptyType(), $type instanceof ReflectionNamedType => $this->buildForNamedType( $property, $type, + $class, $classDefinedDataCollectableAnnotation ), $type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType => $this->buildForMultiType( $property, $type, + $class, $classDefinedDataCollectableAnnotation ), default => throw new TypeError('Invalid reflection type') @@ -62,37 +75,32 @@ public function build( protected function buildForEmptyType(): DataType { - $type = DataType::create(null); - return new DataType( - isNullable: $type->isNullable, - isMixed: $type->isMixed, - isLazy: false, - isOptional: false, - kind: DataTypeKind::Default, - dataClass: null, - dataCollectableClass: null, - acceptedTypes: $type->acceptedTypes, + new UndefinedType(), + false, + false, + DataTypeKind::Default, + null, + null ); } protected function buildForNamedType( ReflectionParameter|ReflectionProperty $reflectionProperty, ReflectionNamedType $reflectionType, + ?string $class, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, ): DataType { - $typeName = $reflectionType->getName(); + $type = SingleType::create($reflectionType, $class); - if (is_a($typeName, Lazy::class, true)) { + if ($type->type->isLazy()) { throw InvalidDataType::onlyLazy($reflectionProperty); } - if (is_a($typeName, Optional::class, true)) { + if ($type->type->isOptional()) { throw InvalidDataType::onlyOptional($reflectionProperty); } - $type = DataType::create($reflectionType); - $kind = DataTypeKind::Default; $dataClass = null; $dataCollectableClass = null; @@ -104,58 +112,46 @@ protected function buildForNamedType( 'dataCollectableClass' => $dataCollectableClass, ] = $this->resolveDataSpecificProperties( $reflectionProperty, - $reflectionType, - $type->acceptedTypes[$typeName], + $type->type, $classDefinedDataCollectableAnnotation ); } return new DataType( - isNullable: $reflectionType->allowsNull(), - isMixed: $type->isMixed, + type: $type, isLazy: false, isOptional: false, kind: $kind, dataClass: $dataClass, - dataCollectableClass: $dataCollectableClass, - acceptedTypes: $type->acceptedTypes, + dataCollectableClass: $dataCollectableClass ); } protected function buildForMultiType( ReflectionParameter|ReflectionProperty $reflectionProperty, ReflectionUnionType|ReflectionIntersectionType $multiReflectionType, + ?string $class, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, ): DataType { - $acceptedTypes = []; - $isNullable = false; + $type = match ($multiReflectionType::class) { + ReflectionUnionType::class => UnionType::create($multiReflectionType, $class), + ReflectionIntersectionType::class => IntersectionType::create($multiReflectionType, $class), + }; + $isLazy = false; $isOptional = false; - $isMixed = false; $kind = DataTypeKind::Default; $dataClass = null; $dataCollectableClass = null; - foreach ($multiReflectionType->getTypes() as $reflectionType) { - $typeName = $reflectionType->getName(); - - $singleType = Type::create($reflectionType); + foreach ($type->types as $subType) { + $isLazy = $isLazy || $subType->isLazy(); + $isOptional = $isOptional || $subType->isOptional(); - $singleTypeIsLazy = is_a($typeName, Lazy::class, true); - $singleTypeIsOptional = is_a($typeName, Optional::class, true); - - $isNullable = $isNullable || $singleType->isNullable; - $isMixed = $isMixed || $singleType->isMixed; - $isLazy = $isLazy || $singleTypeIsLazy; - $isOptional = $isOptional || $singleTypeIsOptional; - - if ($typeName - && array_key_exists($typeName, $singleType->acceptedTypes) - && ! $singleTypeIsLazy - && ! $singleTypeIsOptional + if (($subType->builtIn === false || $subType->name === 'array') + && $subType->isLazy() === false + && $subType->isOptional() === false ) { - $acceptedTypes[$typeName] = $singleType->acceptedTypes[$typeName]; - if ($kind !== DataTypeKind::Default) { continue; } @@ -166,52 +162,36 @@ protected function buildForMultiType( 'dataCollectableClass' => $dataCollectableClass, ] = $this->resolveDataSpecificProperties( $reflectionProperty, - $reflectionType, - $singleType->acceptedTypes[$typeName], + $subType, $classDefinedDataCollectableAnnotation ); } } - if ($kind->isDataObject() && count($acceptedTypes) > 1) { + if ($kind->isDataObject() && $type->acceptedTypesCount() > 1) { throw InvalidDataType::unionWithData($reflectionProperty); } - if ($kind->isDataCollectable() && count($acceptedTypes) > 1) { + if ($kind->isDataCollectable() && $type->acceptedTypesCount() > 1) { throw InvalidDataType::unionWithDataCollection($reflectionProperty); } return new DataType( - isNullable: $isNullable, - isMixed: false, + type: $type, isLazy: $isLazy, isOptional: $isOptional, kind: $kind, dataClass: $dataClass, dataCollectableClass: $dataCollectableClass, - acceptedTypes: $acceptedTypes ); } protected function resolveDataSpecificProperties( ReflectionParameter|ReflectionProperty $reflectionProperty, - ReflectionNamedType $reflectionType, - array $baseTypes, + PartialType $partialType, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, ): array { - $typeName = $reflectionType->getName(); - - $kind = match (true) { - in_array(BaseData::class, $baseTypes) => DataTypeKind::DataObject, - $typeName === 'array' => DataTypeKind::Array, - in_array(Enumerable::class, $baseTypes) => DataTypeKind::Enumerable, - in_array(DataCollection::class, $baseTypes) || $typeName === DataCollection::class => DataTypeKind::DataCollection, - in_array(PaginatedDataCollection::class, $baseTypes) || $typeName === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, - in_array(CursorPaginatedDataCollection::class, $baseTypes) || $typeName === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, - in_array(Paginator::class, $baseTypes) || in_array(AbstractPaginator::class, $baseTypes) => DataTypeKind::Paginator, - in_array(CursorPaginator::class, $baseTypes) || in_array(AbstractCursorPaginator::class, $baseTypes) => DataTypeKind::CursorPaginator, - default => DataTypeKind::Default, - }; + $kind = $partialType->getDataTypeKind(); if ($kind === DataTypeKind::Default) { return [ @@ -224,7 +204,7 @@ protected function resolveDataSpecificProperties( if ($kind === DataTypeKind::DataObject) { return [ 'kind' => DataTypeKind::DataObject, - 'dataClass' => $typeName, + 'dataClass' => $partialType->name, 'dataCollectableClass' => null, ]; } @@ -249,7 +229,7 @@ protected function resolveDataSpecificProperties( return [ 'kind' => $kind, 'dataClass' => $dataClass, - 'dataCollectableClass' => $typeName, + 'dataCollectableClass' => $partialType->name, ]; } @@ -266,16 +246,4 @@ protected function resolveDataSpecificProperties( $reflectionProperty->name ); } - - protected function resolveBaseTypes(string $type): array - { - if (! class_exists($type)) { - return []; - } - - return array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]); - } } diff --git a/src/Support/Type.php b/src/Support/Type.php deleted file mode 100644 index 9553c72e..00000000 --- a/src/Support/Type.php +++ /dev/null @@ -1,165 +0,0 @@ -getName() !== 'mixed' && $typeReflection->getName() !== 'null'; - - return new self( - isNullable: $typeReflection->allowsNull(), - isMixed: $typeReflection->getName() === 'mixed', - acceptedTypes: $hasAcceptedTypes - ? [$typeReflection->getName() => self::resolveBaseTypes($typeReflection->getName())] - : [] - ); - } - - if (! $typeReflection instanceof ReflectionUnionType && ! $typeReflection instanceof ReflectionIntersectionType) { - throw new Exception('Cannot create type'); - } - - // TODO: basically rewrite of DataTypefactory, let's rewrite the whole type system - - $isNullable = false; - $isMixed = false; - $acceptedTypes = []; - - foreach ($typeReflection->getTypes() as $subTypeReflection) { - $subType = static::create($subTypeReflection); - - $isNullable = $isNullable || $subType->isNullable; - $isMixed = $isMixed || $subType->isMixed; - $acceptedTypes = [...$acceptedTypes, ...$subType->acceptedTypes]; - } - - return new self( - isNullable: $isNullable, - isMixed: $isMixed, - acceptedTypes: $acceptedTypes - ); - } - - public function isEmpty(): bool - { - return $this->count() === 0; - } - - public function count(): int - { - return count($this->acceptedTypes); - } - - public function acceptsValue(mixed $value): bool - { - if ($this->isNullable && $value === null) { - return true; - } - - $type = gettype($value); - - $type = match ($type) { - 'integer' => 'int', - 'boolean' => 'bool', - 'double' => 'float', - 'object' => $value::class, - default => $type, - }; - - return $this->acceptsType($type); - } - - - public function acceptsType(string $type): bool - { - if ($this->isMixed) { - return true; - } - - if (array_key_exists($type, $this->acceptedTypes)) { - return true; - } - - if (in_array($type, ['string', 'int', 'bool', 'float', 'array'])) { - return false; - } - - $baseTypes = class_exists($type) - ? array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]) - : []; - - foreach ([$type, ...$baseTypes] as $givenType) { - if (array_key_exists($givenType, $this->acceptedTypes)) { - return true; - } - } - - return false; - } - - public function findAcceptedTypeForBaseType(string $class): ?string - { - foreach ($this->acceptedTypes as $acceptedType => $acceptedBaseTypes) { - if ($class === $acceptedType) { - return $acceptedType; - } - - if (in_array($class, $acceptedBaseTypes)) { - return $acceptedType; - } - } - - return null; - } - - protected static function resolveBaseTypes(string $type): array - { - if (! class_exists($type)) { - return []; - } - - return array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]); - } -} diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index b38efc45..b3f95116 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -110,7 +110,7 @@ protected function resolveTypeForProperty( default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') }; - if ($dataProperty->type->isNullable) { + if ($dataProperty->type->isNullable()) { return new Nullable($collectionType); } diff --git a/src/Support/Types/IntersectionType.php b/src/Support/Types/IntersectionType.php new file mode 100644 index 00000000..f7cd4ff0 --- /dev/null +++ b/src/Support/Types/IntersectionType.php @@ -0,0 +1,35 @@ +isMixed) { + return true; + } + + foreach ($this->types as $subType) { + if (! $subType->acceptsType($type)) { + return false; + } + } + + return true; + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + foreach ($this->types as $subType) { + if ($subType->findAcceptedTypeForBaseType($class) === null) { + return null; + } + } + + return $class; + } +} diff --git a/src/Support/Types/MultiType.php b/src/Support/Types/MultiType.php new file mode 100644 index 00000000..5f579dba --- /dev/null +++ b/src/Support/Types/MultiType.php @@ -0,0 +1,67 @@ +allowsNull(); + $isMixed = false; + $types = []; + + foreach ($multiType->getTypes() as $type) { + if ($type->getName() === 'null') { + continue; + } + + if ($type->getName() === 'mixed') { + $isMixed = true; + } + + $types[] = PartialType::create($type, $class); + } + + return new static( + $isNullable, + $isMixed, + $types + ); + } + + public function getAcceptedTypes(): array + { + $types = []; + + foreach ($this->types as $type) { + $types[$type->name] = $type->acceptedTypes; + } + + return $types; + } + + public function acceptedTypesCount(): int + { + return count(array_filter( + $this->types, + fn(PartialType $subType) => ! $subType->isLazy() && ! $subType->isOptional() + )); + } +} diff --git a/src/Support/Types/PartialType.php b/src/Support/Types/PartialType.php new file mode 100644 index 00000000..6bf8af38 --- /dev/null +++ b/src/Support/Types/PartialType.php @@ -0,0 +1,116 @@ +getName(); + + if ($typeName === 'mixed' || $type->isBuiltin()) { + return new self($typeName, true, []); + } + + if ($typeName === 'self' || $typeName === 'static') { + $typeName = $class; + } + + $acceptedTypes = array_unique([ + ...array_values(class_parents($typeName)), + ...array_values(class_implements($typeName)), + ]); + + return new self( + name: $typeName, + builtIn: $type->isBuiltin(), + acceptedTypes: $acceptedTypes + ); + } + + public function acceptsType(string $type): bool + { + if ($type === $this->name) { + return true; + } + + if ($this->builtIn) { + return false; + } + + // TODO: move this to some store for caching? + $baseTypes = class_exists($type) + ? array_unique([ + ...array_values(class_parents($type)), + ...array_values(class_implements($type)), + ]) + : []; + + if (in_array($this->name, [$type, ...$baseTypes], true)) { + return true; + } + + return false; + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + if ($class === $this->name) { + return $class; + } + + if (in_array($class, $this->acceptedTypes)) { + return $this->name; + } + + return null; + } + + public function isLazy(): bool + { + return $this->name === Lazy::class || in_array(Lazy::class, $this->acceptedTypes); + } + + public function isOptional(): bool + { + return $this->name === Optional::class || in_array(Optional::class, $this->acceptedTypes); + } + + public function getDataTypeKind(): DataTypeKind + { + return match (true) { + in_array(BaseData::class, $this->acceptedTypes) => DataTypeKind::DataObject, + $this->name === 'array' => DataTypeKind::Array, + in_array(Enumerable::class, $this->acceptedTypes) => DataTypeKind::Enumerable, + in_array(DataCollection::class, $this->acceptedTypes) || $this->name === DataCollection::class => DataTypeKind::DataCollection, + in_array(PaginatedDataCollection::class, $this->acceptedTypes) || $this->name === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, + in_array(CursorPaginatedDataCollection::class, $this->acceptedTypes) || $this->name === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, + in_array(Paginator::class, $this->acceptedTypes) || in_array(AbstractPaginator::class, $this->acceptedTypes) => DataTypeKind::Paginator, + in_array(CursorPaginator::class, $this->acceptedTypes) || in_array(AbstractCursorPaginator::class, $this->acceptedTypes) => DataTypeKind::CursorPaginator, + default => DataTypeKind::Default, + }; + } +} diff --git a/src/Support/Types/SingleType.php b/src/Support/Types/SingleType.php new file mode 100644 index 00000000..88bce5db --- /dev/null +++ b/src/Support/Types/SingleType.php @@ -0,0 +1,58 @@ +getName() === 'null') { + throw new Exception('Cannot create a single null type'); + } + + return new self( + isNullable: $reflectionType->allowsNull(), + isMixed: $reflectionType->getName() === 'mixed', + type: PartialType::create($reflectionType, $class) + ); + } + + public function acceptsType(string $type): bool + { + if ($this->isMixed) { + return true; + } + + return $this->type->acceptsType($type); + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + return $this->type->findAcceptedTypeForBaseType($class); + } + + public function getAcceptedTypes(): array + { + if($this->isMixed){ + return []; + } + + return [ + $this->type->name => $this->type->acceptedTypes + ]; + } +} diff --git a/src/Support/Types/Type.php b/src/Support/Types/Type.php new file mode 100644 index 00000000..ee882d4b --- /dev/null +++ b/src/Support/Types/Type.php @@ -0,0 +1,59 @@ + SingleType::create($type, $class), + $type instanceof ReflectionUnionType => UnionType::create($type, $class), + $type instanceof ReflectionIntersectionType => IntersectionType::create($type, $class), + default => new UndefinedType(), + }; + } + + abstract public function acceptsType(string $type): bool; + + abstract public function findAcceptedTypeForBaseType(string $class): ?string; + + // TODO: remove this? + abstract public function getAcceptedTypes(): array; + + public function acceptsValue(mixed $value): bool + { + if ($this->isNullable && $value === null) { + return true; + } + + $type = gettype($value); + + $type = match ($type) { + 'integer' => 'int', + 'boolean' => 'bool', + 'double' => 'float', + 'object' => $value::class, + default => $type, + }; + + return $this->acceptsType($type); + } + +} diff --git a/src/Support/Types/UndefinedType.php b/src/Support/Types/UndefinedType.php new file mode 100644 index 00000000..3792fe9f --- /dev/null +++ b/src/Support/Types/UndefinedType.php @@ -0,0 +1,26 @@ +isMixed) { + return true; + } + + foreach ($this->types as $subType) { + if ($subType->acceptsType($type)) { + return true; + } + } + + return false; + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + foreach ($this->types as $subType) { + if ($found = $subType->findAcceptedTypeForBaseType($class)) { + return $found; + } + } + + return null; + } +} diff --git a/tests/Resolvers/EmptyDataResolverTest.php b/tests/Resolvers/EmptyDataResolverTest.php index 722dfcf9..c9b0fa3d 100644 --- a/tests/Resolvers/EmptyDataResolverTest.php +++ b/tests/Resolvers/EmptyDataResolverTest.php @@ -97,13 +97,13 @@ function assertEmptyPropertyValue( public Lazy|string|null $property; }); - assertEmptyPropertyValue([], new class () { - public Lazy|array|null $property; - }); - - assertEmptyPropertyValue(['string' => null], new class () { - public Lazy|SimpleData|null $property; - }); +// assertEmptyPropertyValue([], new class () { +// public Lazy|array|null $property; +// }); +// +// assertEmptyPropertyValue(['string' => null], new class () { +// public Lazy|SimpleData|null $property; +// }); }); it('will return the base type for lazy types that can be optional', function () { diff --git a/tests/Support/DataMethodTest.php b/tests/Support/DataMethodTest.php index d7ee0b2f..9014ecc3 100644 --- a/tests/Support/DataMethodTest.php +++ b/tests/Support/DataMethodTest.php @@ -72,8 +72,12 @@ public static function collectArray( ->isPublic->toBeTrue() ->isStatic->toBeTrue() ->customCreationMethodType->toBe(CustomCreationMethodType::Collection) - ->returnType->toEqual(new Type(isNullable: false, isMixed: false, acceptedTypes: ['array' => []])) ->and($method->parameters[0])->toBeInstanceOf(DataParameter::class); + + expect($method->returnType) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toBe(['array' => []]); }); it('can create a data method from a magic collect method with nullable return type', function () { @@ -87,8 +91,12 @@ public static function collectArray( $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); expect($method) - ->customCreationMethodType->toBe(CustomCreationMethodType::Collection) - ->returnType->toEqual(new Type(isNullable: true, isMixed: false, acceptedTypes: ['array' => []])); + ->customCreationMethodType->toBe(CustomCreationMethodType::Collection); + + expect($method->returnType) + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toBe(['array' => []]); }); it('will not create a magical collection method when no return type specified', function () { @@ -102,8 +110,12 @@ public static function collectArray( $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); expect($method) - ->customCreationMethodType->toBe(CustomCreationMethodType::None) - ->returnType->toEqual(new Type(isNullable: true, isMixed: true, acceptedTypes: [])); + ->customCreationMethodType->toBe(CustomCreationMethodType::None); + + expect($method->returnType) + ->isNullable->toBeTrue() + ->isMixed->toBeTrue() + ->getAcceptedTypes()->toBe([]); }); it('correctly accepts single values as magic creation method', function () { diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index 27c67dc0..6ecee392 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -54,14 +54,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeTrue() - ->isMixed->toBeTrue() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toBeEmpty(); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isMixed->toBeTrue() + ->isNullable->toBeTrue() + ->getAcceptedTypes()->toBe([]); }); it('can deduce a type with definition', function () { @@ -70,14 +72,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys(['string']); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isMixed->toBeFalse() + ->isNullable->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['string']); }); it('can deduce a nullable type with definition', function () { @@ -86,14 +90,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectionClass->toBeNull() - ->acceptedTypes->toHaveKeys(['string']); + ->dataCollectionClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['string']); }); it('can deduce a union type definition', function () { @@ -102,14 +108,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys(['string', 'int']); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['string', 'int']); }); it('can deduce a nullable union type definition', function () { @@ -118,14 +126,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys(['string', 'int']); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['string', 'int']); }); it('can deduce an intersection type definition', function () { @@ -134,14 +144,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys([ + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([ DateTime::class, DateTimeImmutable::class, ]); @@ -153,14 +165,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeTrue() - ->isMixed->toBeTrue() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys([]); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeTrue() + ->isMixed->toBeTrue() + ->getAcceptedTypes()->toHaveKeys([]); }); it('can deduce a lazy type', function () { @@ -169,14 +183,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys(['string']); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['string']); }); it('can deduce an optional type', function () { @@ -185,14 +201,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeTrue() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys(['string']); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['string']); }); test('a type cannot be optional alone', function () { @@ -207,14 +225,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataObject) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys([SimpleData::class]); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); }); it('can deduce a data union type', function () { @@ -223,14 +243,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataObject) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBeNull() - ->acceptedTypes->toHaveKeys([SimpleData::class]); + ->dataCollectableClass->toBeNull(); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); }); it('can deduce a data collection type', function () { @@ -240,14 +262,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class) - ->acceptedTypes->toHaveKeys([DataCollection::class]); + ->dataCollectableClass->toBe(DataCollection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); }); it('can deduce a data collection union type', function () { @@ -257,14 +281,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class) - ->acceptedTypes->toHaveKeys([DataCollection::class]); + ->dataCollectableClass->toBe(DataCollection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); }); it('can deduce a paginated data collection type', function () { @@ -274,14 +300,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(PaginatedDataCollection::class) - ->acceptedTypes->toHaveKeys([PaginatedDataCollection::class]); + ->dataCollectableClass->toBe(PaginatedDataCollection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); }); it('can deduce a paginated data collection union type', function () { @@ -291,14 +319,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(PaginatedDataCollection::class) - ->acceptedTypes->toHaveKeys([PaginatedDataCollection::class]); + ->dataCollectableClass->toBe(PaginatedDataCollection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); }); it('can deduce a cursor paginated data collection type', function () { @@ -308,14 +338,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) - ->acceptedTypes->toHaveKeys([CursorPaginatedDataCollection::class]); + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); }); it('can deduce a cursor paginated data collection union type', function () { @@ -325,14 +357,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) - ->acceptedTypes->toHaveKeys([CursorPaginatedDataCollection::class]); + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); }); it('can deduce an array data collection type', function () { @@ -342,14 +376,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe('array') - ->acceptedTypes->toHaveKeys(['array']); + ->dataCollectableClass->toBe('array'); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['array']); }); it('can deduce an array data collection union type', function () { @@ -359,14 +395,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe('array') - ->acceptedTypes->toHaveKeys(['array']); + ->dataCollectableClass->toBe('array'); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys(['array']); }); it('can deduce an enumerable data collection type', function () { @@ -376,14 +414,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(Collection::class) - ->acceptedTypes->toHaveKeys([Collection::class]); + ->dataCollectableClass->toBe(Collection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([Collection::class]); }); it('can deduce an enumerable data collection union type', function () { @@ -393,14 +433,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(Collection::class) - ->acceptedTypes->toHaveKeys([Collection::class]); + ->dataCollectableClass->toBe(Collection::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([Collection::class]); }); it('can deduce a paginator data collection type', function () { @@ -410,14 +452,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(LengthAwarePaginator::class) - ->acceptedTypes->toHaveKeys([LengthAwarePaginator::class]); + ->dataCollectableClass->toBe(LengthAwarePaginator::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); }); it('can deduce a paginator data collection union type', function () { @@ -427,14 +471,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(LengthAwarePaginator::class) - ->acceptedTypes->toHaveKeys([LengthAwarePaginator::class]); + ->dataCollectableClass->toBe(LengthAwarePaginator::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); }); it('can deduce a cursor paginator data collection type', function () { @@ -444,14 +490,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeFalse() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginator::class) - ->acceptedTypes->toHaveKeys([CursorPaginator::class]); + ->dataCollectableClass->toBe(CursorPaginator::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); }); it('can deduce a cursor paginator data collection union type', function () { @@ -461,14 +509,16 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() ->isLazy->toBeTrue() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginator::class) - ->acceptedTypes->toHaveKeys([CursorPaginator::class]); + ->dataCollectableClass->toBe(CursorPaginator::class); + + expect($type->type) + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); }); it('cannot have multiple data types', function () { @@ -493,7 +543,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType it( 'will resolve the base types for accepted types', function (object $class, array $expected) { - expect(resolveDataType($class)->acceptedTypes)->toEqualCanonicalizing($expected); + expect(resolveDataType($class)->type->getAcceptedTypes())->toEqualCanonicalizing($expected); } )->with(function () { yield 'no type' => [ @@ -571,7 +621,7 @@ function (object $class, array $expected) { it( 'can check if a data type accepts a type', function (object $class, string $type, bool $accepts) { - expect(resolveDataType($class))->acceptsType($type)->toEqual($accepts); + expect(resolveDataType($class))->type->acceptsType($type)->toEqual($accepts); } )->with(function () { // Base types @@ -724,7 +774,7 @@ function (object $class, string $type, bool $accepts) { it( 'can check if a data type accepts a value', function (object $class, mixed $value, bool $accepts) { - expect(resolveDataType($class))->acceptsValue($value)->toEqual($accepts); + expect(resolveDataType($class))->type->acceptsValue($value)->toEqual($accepts); } )->with(function () { yield [ @@ -796,7 +846,9 @@ function (object $class, mixed $value, bool $accepts) { 'can find accepted type for a base type', function (object $class, string $type, ?string $expectedType) { expect(resolveDataType($class)) - ->findAcceptedTypeForBaseType($type)->toEqual($expectedType); + ->type + ->findAcceptedTypeForBaseType($type) + ->toEqual($expectedType); } )->with(function () { yield [ From 51ac1d492312fdf9c870a43e1bbfbdb55c5ffb1b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 22 Feb 2023 10:59:21 +0100 Subject: [PATCH 006/124] wip --- src/Concerns/BaseData.php | 19 -- src/Concerns/DeprecatedData.php | 38 ++++ src/Contracts/BaseData.php | 7 - src/Contracts/DeprecatedData.php | 25 +++ src/DataPipes/CastPropertiesDataPipe.php | 4 +- .../DataCollectableFromSomethingResolver.php | 194 ++++++++++++++++-- src/Support/DataClass.php | 3 +- .../DataCollectables/ArrayDataCollectable.php | 16 ++ .../DataCollectables/DataCollectable.php | 10 + .../DataCollectionDataCollectable.php | 21 ++ src/Support/DataMethod.php | 27 ++- src/Support/DataParameter.php | 9 +- src/Support/Types/PartialType.php | 33 ++- src/Support/Types/SingleType.php | 4 +- tests/DataCollectionTest.php | 92 +++++---- .../DataPipes/CastPropertiesDataPipeTest.php | 58 ++++-- tests/DataPipes/MapPropertiesDataPipeTest.php | 10 +- tests/DataTest.php | 143 ++++++++----- tests/Datasets/DataCollection.php | 9 +- .../Casts/ConfidentialDataCollectionCast.php | 4 +- tests/Fakes/ComplicatedData.php | 3 + tests/Fakes/DataWithMapper.php | 2 +- tests/Fakes/EmptyData.php | 25 --- tests/Fakes/MultiNestedData.php | 2 +- .../ConfidentialDataCollectionTransformer.php | 4 +- tests/RequestDataTest.php | 5 +- .../PartialsTreeFromRequestResolverTest.php | 4 +- tests/Support/DataParameterTest.php | 17 +- tests/Support/DataTypeTest.php | 8 +- .../DataCollectionEloquentCastTest.php | 11 +- .../DataTypeScriptTransformerTest.php | 9 +- 31 files changed, 580 insertions(+), 236 deletions(-) create mode 100644 src/Concerns/DeprecatedData.php create mode 100644 src/Contracts/DeprecatedData.php create mode 100644 src/Support/DataCollectables/ArrayDataCollectable.php create mode 100644 src/Support/DataCollectables/DataCollectable.php create mode 100644 src/Support/DataCollectables/DataCollectionDataCollectable.php delete mode 100644 tests/Fakes/EmptyData.php diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 3356c5fe..eac2d00e 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -37,12 +37,6 @@ trait BaseData { - protected static string $_collectionClass = DataCollection::class; - - protected static string $_paginatedCollectionClass = PaginatedDataCollection::class; - - protected static string $_cursorPaginatedCollectionClass = CursorPaginatedDataCollection::class; - protected ?DataContext $_dataContext = null; public static function optional(mixed ...$payloads): ?static @@ -116,19 +110,6 @@ public static function prepareForPipeline(Collection $properties): Collection return $properties; } - public static function collection(Enumerable|array|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator|DataCollection $items): DataCollection|CursorPaginatedDataCollection|PaginatedDataCollection - { - if ($items instanceof Paginator || $items instanceof AbstractPaginator) { - return new (static::$_paginatedCollectionClass)(static::class, $items); - } - - if ($items instanceof AbstractCursorPaginator || $items instanceof CursorPaginator) { - return new (static::$_cursorPaginatedCollectionClass)(static::class, $items); - } - - return new (static::$_collectionClass)(static::class, $items); - } - public function __sleep(): array { return app(DataConfig::class)->getDataClass(static::class) diff --git a/src/Concerns/DeprecatedData.php b/src/Concerns/DeprecatedData.php new file mode 100644 index 00000000..0ad3a6ac --- /dev/null +++ b/src/Concerns/DeprecatedData.php @@ -0,0 +1,38 @@ +|TValue[]|\Illuminate\Pagination\AbstractPaginator|\Illuminate\Contracts\Pagination\Paginator|\Illuminate\Pagination\AbstractCursorPaginator|\Illuminate\Contracts\Pagination\CursorPaginator|\Spatie\LaravelData\DataCollection $items - * - * @return ($items is \Illuminate\Pagination\AbstractCursorPaginator|\Illuminate\Pagination\CursorPaginator ? \Spatie\LaravelData\CursorPaginatedDataCollection : ($items is \Illuminate\Pagination\Paginator|\Illuminate\Pagination\AbstractPaginator ? \Spatie\LaravelData\PaginatedDataCollection : \Spatie\LaravelData\DataCollection)) - */ - public static function collection(Enumerable|array|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator|DataCollection $items): DataCollection|CursorPaginatedDataCollection|PaginatedDataCollection; - public static function normalizers(): array; public static function pipeline(): DataPipeline; diff --git a/src/Contracts/DeprecatedData.php b/src/Contracts/DeprecatedData.php new file mode 100644 index 00000000..8166dc59 --- /dev/null +++ b/src/Contracts/DeprecatedData.php @@ -0,0 +1,25 @@ +|TValue[]|\Illuminate\Pagination\AbstractPaginator|\Illuminate\Contracts\Pagination\Paginator|\Illuminate\Pagination\AbstractCursorPaginator|\Illuminate\Contracts\Pagination\CursorPaginator|\Spatie\LaravelData\DataCollection $items + * + * @return ($items is \Illuminate\Pagination\AbstractCursorPaginator|\Illuminate\Pagination\CursorPaginator ? \Spatie\LaravelData\CursorPaginatedDataCollection : ($items is \Illuminate\Pagination\Paginator|\Illuminate\Pagination\AbstractPaginator ? \Spatie\LaravelData\PaginatedDataCollection : \Spatie\LaravelData\DataCollection)) + */ + public static function collection(Enumerable|array|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator|DataCollection $items): DataCollection|CursorPaginatedDataCollection|PaginatedDataCollection; +} diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 7ca6ce9f..bd876cfc 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -63,9 +63,7 @@ protected function cast( } if ($property->type->kind->isDataCollectable()) { - return $property->type->dataClass::collection($value); - // TODO: future - // return $property->type->dataClass::collect($value, $property->type->dataCollectableClass); + return $property->type->dataClass::collect($value, $property->type->dataCollectableClass); } return $value; diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 80f037ef..a6275d87 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData\Resolvers; +use Closure; use Exception; use Illuminate\Contracts\Pagination\CursorPaginator; use Illuminate\Contracts\Pagination\Paginator; @@ -14,11 +15,13 @@ use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\CustomCreationMethodType; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotCreateDataCollectable; use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; +use Spatie\LaravelData\Support\Types\PartialType; class DataCollectableFromSomethingResolver { @@ -49,13 +52,9 @@ public function execute( mixed $items, ?string $into = null, ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { - $from = match (gettype($items)) { - 'object' => $items::class, - 'array' => 'array', - default => throw new Exception('Unknown type provided to create a collectable') - }; - - $into ??= $from; + $intoType = $into !== null + ? PartialType::createFromTypeString($into) + : PartialType::createFromValue($items); $collectable = $this->createFromCustomCreationMethod($dataClass, $items, $into); @@ -63,15 +62,43 @@ public function execute( return $collectable; } - if ($into === 'array') { - return $this->createArray($dataClass, $from, $items); + $dataTypeKind = $intoType->getDataTypeKind(); + + if ($dataTypeKind === DataTypeKind::Array) { + return $this->createArray($dataClass, $items); + } + + if ($dataTypeKind === DataTypeKind::Enumerable) { + return $this->createEnumerable($dataClass, $items, $intoType); + } + + if ($dataTypeKind === DataTypeKind::DataCollection) { + return $this->createDataCollection($dataClass, $items, $intoType); + } + + if ($dataTypeKind === DataTypeKind::Paginator) { + return $this->createPaginator($dataClass, $items, $intoType); + } + + if ($dataTypeKind === DataTypeKind::DataPaginatedCollection) { + return $this->createPaginatedDataCollection($dataClass, $items, $intoType); + } + + if ($dataTypeKind === DataTypeKind::CursorPaginator) { + return $this->createCursorPaginator($dataClass, $items, $intoType); } + + if ($dataTypeKind === DataTypeKind::DataCursorPaginatedCollection) { + return $this->createCursorPaginatedDataCollection($dataClass, $items, $intoType); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name); } protected function createFromCustomCreationMethod( string $dataClass, mixed $items, - string $into, + ?string $into, ): null|array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { if ($this->withoutMagicalCreation) { return null; @@ -81,16 +108,23 @@ protected function createFromCustomCreationMethod( $method = $this->dataConfig ->getDataClass($dataClass) ->methods - ->filter( - fn(DataMethod $method) => $method->customCreationMethodType === CustomCreationMethodType::Collection - && ! in_array($method->name, $this->ignoredMagicalMethods) - && $method->returns($into) - && $method->accepts([$items]) - ) + ->filter(function (DataMethod $method) use ($into, $items) { + if ($method->customCreationMethodType !== CustomCreationMethodType::Collection) { + return false; + } + + if ($into !== null && ! $method->returns($into)) { + return false; + } + + return $method->accepts([$items]); + }) ->first(); if ($method !== null) { - return $dataClass::{$method->name}($items); + return $dataClass::{$method->name}( + array_map($this->itemsToDataClosure($dataClass), $items) + ); } return null; @@ -98,7 +132,6 @@ protected function createFromCustomCreationMethod( protected function createArray( string $dataClass, - string $from, mixed $items ): array { if ($items instanceof DataCollection) { @@ -111,11 +144,132 @@ protected function createArray( if (is_array($items)) { return array_map( - fn(mixed $data) => $dataClass::from($data), + $this->itemsToDataClosure($dataClass), $items, ); } - throw CannotCreateDataCollectable::create($from, 'array'); + throw CannotCreateDataCollectable::create(get_debug_type($items), 'array'); + } + + protected function createEnumerable( + string $dataClass, + mixed $items, + PartialType $intoType, + ): Enumerable { + if ($items instanceof DataCollection) { + $items = $items->items(); + } + + if (is_array($items)) { + return new $intoType->name($items); + } + + if ($items instanceof Enumerable) { + return $items->map( + $this->itemsToDataClosure($dataClass) + ); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), Enumerable::class); + } + + protected function createDataCollection( + string $dataClass, + mixed $items, + PartialType $intoType, + ): DataCollection { + if ($items instanceof Enumerable) { + $items = $items->all(); + } + + if (is_array($items)) { + return new $intoType->name($dataClass, $items); + } + + if ($items instanceof DataCollection) { + return $items->map( + $this->itemsToDataClosure($dataClass) + ); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), DataCollection::class); + } + + protected function createPaginator( + string $dataClass, + mixed $items, + PartialType $intoType, + ): AbstractPaginator|Paginator { + if ($items instanceof PaginatedDataCollection) { + $items = $items->items(); + } + + if ($items instanceof AbstractPaginator || $items instanceof Paginator) { + return $items->through( + $this->itemsToDataClosure($dataClass) + ); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), Paginator::class); + } + + protected function createPaginatedDataCollection( + string $dataClass, + mixed $items, + PartialType $intoType, + ): PaginatedDataCollection { + if ($items instanceof AbstractPaginator || $items instanceof Paginator) { + return new $intoType->name($dataClass, $items); + } + + if ($items instanceof PaginatedDataCollection) { + return $items->through( + $this->itemsToDataClosure($dataClass) + ); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), PaginatedDataCollection::class); + } + + protected function createCursorPaginator( + string $dataClass, + mixed $items, + PartialType $intoType, + ): CursorPaginator|AbstractCursorPaginator { + if ($items instanceof CursorPaginatedDataCollection) { + $items = $items->items(); + } + + if ($items instanceof AbstractCursorPaginator || $items instanceof CursorPaginator) { + return $items->through( + $this->itemsToDataClosure($dataClass) + ); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), CursorPaginator::class); + } + + protected function createCursorPaginatedDataCollection( + string $dataClass, + mixed $items, + PartialType $intoType, + ): CursorPaginatedDataCollection { + if ($items instanceof AbstractCursorPaginator || $items instanceof CursorPaginator) { + return new $intoType->name($dataClass, $items); + } + + if ($items instanceof CursorPaginatedDataCollection) { + return $items->through( + $this->itemsToDataClosure($dataClass) + ); + } + + throw CannotCreateDataCollectable::create(get_debug_type($items), CursorPaginatedDataCollection::class); + } + + protected function itemsToDataClosure(string $dataClass): Closure + { + return fn(mixed $data) => $dataClass::from($data); } } diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 5d30d490..744ec672 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -85,7 +85,8 @@ protected static function resolveMethods( ReflectionClass $reflectionClass, ): Collection { return collect($reflectionClass->getMethods()) - ->filter(fn(ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collectFrom')) + ->filter(fn(ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collect')) + ->reject(fn(ReflectionMethod $method) => in_array($method->name, ['from', 'collect', 'collection'])) ->mapWithKeys( fn(ReflectionMethod $method) => [$method->name => DataMethod::create($method)], ); diff --git a/src/Support/DataCollectables/ArrayDataCollectable.php b/src/Support/DataCollectables/ArrayDataCollectable.php new file mode 100644 index 00000000..f832e76c --- /dev/null +++ b/src/Support/DataCollectables/ArrayDataCollectable.php @@ -0,0 +1,16 @@ +items(); + } + + public function denormalize(array $items, string $dataClass, string $collectableClass): DataCollection + { + return new $collectableClass($dataClass, $items); + } +} diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index f7b4b25d..30d3b0b0 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -9,6 +9,7 @@ use ReflectionParameter; use ReflectionUnionType; use Spatie\LaravelData\Enums\CustomCreationMethodType; +use Spatie\LaravelData\Support\Types\Type; use Spatie\LaravelData\Support\Types\UndefinedType; /** @@ -22,13 +23,13 @@ public function __construct( public readonly bool $isStatic, public readonly bool $isPublic, public readonly CustomCreationMethodType $customCreationMethodType, - public readonly \Spatie\LaravelData\Support\Types\Type $returnType, + public readonly Type $returnType, ) { } public static function create(ReflectionMethod $method): self { - $returnType = \Spatie\LaravelData\Support\Types\Type::forReflection( + $returnType = Type::forReflection( $method->getReturnType(), $method->class, ); @@ -36,7 +37,7 @@ public static function create(ReflectionMethod $method): self return new self( $method->name, collect($method->getParameters())->map( - fn(ReflectionParameter $parameter) => DataParameter::create($parameter), + fn(ReflectionParameter $parameter) => DataParameter::create($parameter, $method->class), ), $method->isStatic(), $method->isPublic(), @@ -51,12 +52,12 @@ public static function createConstructor(?ReflectionMethod $method, Collection $ return null; } - $parameters = collect($method->getParameters())->map(function (ReflectionParameter $parameter) use ($properties) { + $parameters = collect($method->getParameters())->map(function (ReflectionParameter $parameter) use ($method, $properties) { if ($parameter->isPromoted()) { return $properties->get($parameter->name); } - return DataParameter::create($parameter); + return DataParameter::create($parameter, $method->class); }); return new self( @@ -71,7 +72,7 @@ public static function createConstructor(?ReflectionMethod $method, Collection $ protected static function resolveCustomCreationMethodType( ReflectionMethod $method, - ?\Spatie\LaravelData\Support\Types\Type $returnType, + ?Type $returnType, ): CustomCreationMethodType { if (! $method->isStatic() || ! $method->isPublic() @@ -95,7 +96,7 @@ protected static function resolveCustomCreationMethodType( public function accepts(mixed ...$input): bool { - /** @var Collection<\Spatie\LaravelData\Support\DataParameter|\Spatie\LaravelData\Support\DataProperty> $parameters */ + /** @var Collection $parameters */ $parameters = array_is_list($input) ? $this->parameters : $this->parameters->mapWithKeys(fn(DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); @@ -115,7 +116,17 @@ public function accepts(mixed ...$input): bool continue; } - if (! $parameter->type->type->acceptsValue($input[$index])) { + if( + $parameter instanceof DataProperty + && ! $parameter->type->type->acceptsValue($input[$index]) + ){ + return false; + } + + if( + $parameter instanceof DataParameter + && ! $parameter->type->acceptsValue($input[$index]) + ){ return false; } } diff --git a/src/Support/DataParameter.php b/src/Support/DataParameter.php index e677b97e..c7ed953d 100644 --- a/src/Support/DataParameter.php +++ b/src/Support/DataParameter.php @@ -3,7 +3,7 @@ namespace Spatie\LaravelData\Support; use ReflectionParameter; -use Spatie\LaravelData\Support\Factories\DataTypeFactory; +use Spatie\LaravelData\Support\Types\Type; class DataParameter { @@ -12,12 +12,13 @@ public function __construct( public readonly bool $isPromoted, public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, - public readonly DataType $type, + public readonly Type $type, ) { } public static function create( - ReflectionParameter $parameter + ReflectionParameter $parameter, + string $class, ): self { $hasDefaultValue = $parameter->isDefaultValueAvailable(); @@ -26,7 +27,7 @@ public static function create( $parameter->isPromoted(), $hasDefaultValue, $hasDefaultValue ? $parameter->getDefaultValue() : null, - DataTypeFactory::create()->build($parameter), + Type::forReflection($parameter->getType(), $class), ); } } diff --git a/src/Support/Types/PartialType.php b/src/Support/Types/PartialType.php index 6bf8af38..c79a8938 100644 --- a/src/Support/Types/PartialType.php +++ b/src/Support/Types/PartialType.php @@ -39,18 +39,31 @@ public static function create( $typeName = $class; } - $acceptedTypes = array_unique([ - ...array_values(class_parents($typeName)), - ...array_values(class_implements($typeName)), - ]); - return new self( name: $typeName, builtIn: $type->isBuiltin(), - acceptedTypes: $acceptedTypes + acceptedTypes: self::resolveAcceptedTypes($typeName) + ); + } + + public static function createFromTypeString(string $type): self + { + $builtIn = in_array($type, ['float', 'bool', 'int', 'array', 'string', 'mixed']); + + return new self( + name: $type, + builtIn: $builtIn, + acceptedTypes: ! $builtIn + ? self::resolveAcceptedTypes($type) + : [] ); } + public static function createFromValue(mixed $value): self + { + return self::createFromTypeString(get_debug_type($value)); + } + public function acceptsType(string $type): bool { if ($type === $this->name) { @@ -113,4 +126,12 @@ public function getDataTypeKind(): DataTypeKind default => DataTypeKind::Default, }; } + + protected static function resolveAcceptedTypes(string $type): array + { + return array_unique([ + ...array_values(class_parents($type)), + ...array_values(class_implements($type)), + ]); + } } diff --git a/src/Support/Types/SingleType.php b/src/Support/Types/SingleType.php index 88bce5db..d81f19fd 100644 --- a/src/Support/Types/SingleType.php +++ b/src/Support/Types/SingleType.php @@ -47,12 +47,12 @@ public function findAcceptedTypeForBaseType(string $class): ?string public function getAcceptedTypes(): array { - if($this->isMixed){ + if ($this->isMixed) { return []; } return [ - $this->type->name => $this->type->acceptedTypes + $this->type->name => $this->type->acceptedTypes, ]; } } diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index ec8b9980..7e0497fa 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -5,11 +5,12 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; +use Spatie\LaravelData\Concerns\DeprecatedData; +use Spatie\LaravelData\Contracts\DeprecatedData as DeprecatedDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\PartialTrees; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; @@ -20,7 +21,7 @@ use function Spatie\Snapshots\assertMatchesSnapshot; it('can get a paginated data collection', function () { - $items = Collection::times(100, fn (int $index) => "Item {$index}"); + $items = Collection::times(100, fn(int $index) => "Item {$index}"); $paginator = new LengthAwarePaginator( $items->forPage(1, 15), @@ -28,21 +29,21 @@ 15 ); - $collection = SimpleData::collection($paginator); + $collection = new PaginatedDataCollection(SimpleData::class, $paginator); expect($collection)->toBeInstanceOf(PaginatedDataCollection::class); assertMatchesJsonSnapshot($collection->toJson()); }); it('can get a paginated cursor data collection', function () { - $items = Collection::times(100, fn (int $index) => "Item {$index}"); + $items = Collection::times(100, fn(int $index) => "Item {$index}"); $paginator = new CursorPaginator( $items, 15, ); - $collection = SimpleData::collection($paginator); + $collection = new CursorPaginatedDataCollection(SimpleData::class, $paginator); if (version_compare(app()->version(), '9.0.0', '<=')) { $this->markTestIncomplete('Laravel 8 uses a different format'); @@ -53,24 +54,24 @@ }); test('a collection can be constructed with data object', function () { - $collectionA = SimpleData::collection([ + $collectionA = new DataCollection(SimpleData::class, [ SimpleData::from('A'), SimpleData::from('B'), ]); - $collectionB = SimpleData::collection([ + $collectionB = SimpleData::collect([ 'A', 'B', - ]); + ], DataCollection::class); expect($collectionB)->toArray() ->toMatchArray($collectionA->toArray()); }); test('a collection can be filtered', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); - $filtered = $collection->filter(fn (SimpleData $data) => $data->string === 'A')->toArray(); + $filtered = $collection->filter(fn(SimpleData $data) => $data->string === 'A')->toArray(); expect([ ['string' => 'A'], @@ -79,9 +80,9 @@ }); test('a collection can be transformed', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); - $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); + $filtered = $collection->through(fn(SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); expect($filtered)->toMatchArray([ ['string' => 'Ax'], @@ -90,11 +91,12 @@ }); test('a paginated collection can be transformed', function () { - $collection = SimpleData::collection( - new LengthAwarePaginator(['A', 'B'], 2, 15) + $collection = new PaginatedDataCollection( + SimpleData::class, + new LengthAwarePaginator(['A', 'B'], 2, 15), ); - $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); + $filtered = $collection->through(fn(SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); expect($filtered['data'])->toMatchArray([ ['string' => 'Ax'], @@ -103,7 +105,7 @@ }); it('is iteratable', function () { - $collection = SimpleData::collection([ + $collection = new DataCollection(SimpleData::class, [ 'A', 'B', 'C', 'D', ]); @@ -153,7 +155,7 @@ it('can dynamically include data based upon the request', function () { LazyData::$allowedIncludes = ['']; - $response = LazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()); + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); expect($response)->getData(true) ->toMatchArray([ @@ -164,7 +166,7 @@ LazyData::$allowedIncludes = ['name']; - $includedResponse = LazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $includedResponse = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', ])); @@ -179,7 +181,7 @@ it('can disabled manually including data in the request', function () { LazyData::$allowedIncludes = []; - $response = LazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', ])); @@ -192,7 +194,7 @@ LazyData::$allowedIncludes = ['name']; - $response = LazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', ])); @@ -205,7 +207,7 @@ LazyData::$allowedIncludes = null; - $response = LazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', ])); @@ -220,7 +222,7 @@ it('can dynamically exclude data based upon the request', function () { DefaultLazyData::$allowedExcludes = []; - $response = DefaultLazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()); + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); expect($response)->getData(true) ->toMatchArray([ @@ -231,7 +233,7 @@ DefaultLazyData::$allowedExcludes = ['name']; - $excludedResponse = DefaultLazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $excludedResponse = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', ])); @@ -246,7 +248,7 @@ it('can disable manually excluding data in the request', function () { DefaultLazyData::$allowedExcludes = []; - $response = DefaultLazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', ])); @@ -259,7 +261,7 @@ DefaultLazyData::$allowedExcludes = ['name']; - $response = DefaultLazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', ])); @@ -272,7 +274,7 @@ DefaultLazyData::$allowedExcludes = null; - $response = DefaultLazyData::collection(['Ruben', 'Freek', 'Brent'])->toResponse(request()->merge([ + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', ])); @@ -285,7 +287,7 @@ }); it('can update data properties withing a collection', function () { - $collection = LazyData::collection([ + $collection = new DataCollection(LazyData::class, [ LazyData::from('Never gonna give you up!'), ]); @@ -329,7 +331,7 @@ } }); - $collection = SimpleData::collection($lazyCollection); + $collection = new DataCollection(SimpleData::class, $lazyCollection); expect($collection)->items() ->toMatchArray([ @@ -341,7 +343,7 @@ $data->string = strtoupper($data->string); return $data; - })->filter(fn (SimpleData $data) => $data->string === strtoupper('Never gonna give you up!'))->toArray(); + })->filter(fn(SimpleData $data) => $data->string === strtoupper('Never gonna give you up!'))->toArray(); expect($transformed)->toMatchArray([ ['string' => strtoupper('Never gonna give you up!')], @@ -357,12 +359,12 @@ ]) ) ->toEqual( - SimpleData::collection(['A', 'B', 'C'])->toCollection() + (new DataCollection(SimpleData::class, ['A', 'B', 'C']))->toCollection() ); }); test('a collection can be transformed to JSON', function () { - $collection = SimpleData::collection(['A', 'B', 'C']); + $collection = (new DataCollection(SimpleData::class, ['A', 'B', 'C'])); expect('[{"string":"A"},{"string":"B"},{"string":"C"}]') ->toEqual($collection->toJson()) @@ -381,7 +383,7 @@ public static function fromSimpleData(SimpleData $simpleData): static } }; - $collection = $dataClass::collection([ + $collection = new DataCollection($dataClass::class, [ SimpleData::from('A'), SimpleData::from('B'), ]); @@ -396,13 +398,13 @@ public static function fromSimpleData(SimpleData $simpleData): static }); it('can reset the keys', function () { - $collection = SimpleData::collection([ + $collection = new DataCollection(SimpleData::class, [ 1 => SimpleData::from('a'), 3 => SimpleData::from('b'), ]); expect( - SimpleData::collection([ + new DataCollection(SimpleData::class, [ 0 => SimpleData::from('a'), 1 => SimpleData::from('b'), ]) @@ -410,7 +412,7 @@ public static function fromSimpleData(SimpleData $simpleData): static }); it('can use magical creation methods to create a collection', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); expect($collection->toCollection()->all()) ->toMatchArray([ @@ -420,7 +422,9 @@ public static function fromSimpleData(SimpleData $simpleData): static }); it('can return a custom data collection when collecting data', function () { - $class = new class ('') extends Data { + $class = new class ('') extends Data implements DeprecatedDataContract { + use DeprecatedData; + protected static string $_collectionClass = CustomDataCollection::class; public function __construct(public string $string) @@ -437,7 +441,9 @@ public function __construct(public string $string) }); it('can return a custom paginated data collection when collecting data', function () { - $class = new class ('') extends Data { + $class = new class ('') extends Data implements DeprecatedDataContract{ + use DeprecatedData; + protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; public function __construct(public string $string) @@ -453,7 +459,7 @@ public function __construct(public string $string) it( 'can perform some collection operations', function (string $operation, array $arguments, array $expected) { - $collection = SimpleData::collection(['A', 'B', 'C']); + $collection = new DataCollection(SimpleData::class, ['A', 'B', 'C']); $changedCollection = $collection->{$operation}(...$arguments); @@ -463,7 +469,7 @@ function (string $operation, array $arguments, array $expected) { )->with('collection-operations'); it('can return a sole data object', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->sole('string', '=', 'A'); @@ -472,7 +478,7 @@ function (string $operation, array $arguments, array $expected) { }); it('can return a sole data object without specifying an operator', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->sole('string', 'A'); @@ -482,7 +488,7 @@ function (string $operation, array $arguments, array $expected) { it('can serialize and unserialize a data collection', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); $serialized = serialize($collection); @@ -491,11 +497,11 @@ function (string $operation, array $arguments, array $expected) { $unserialized = unserialize($serialized); expect($unserialized)->toBeInstanceOf(DataCollection::class); - expect($unserialized)->toEqual(SimpleData::collection(['A', 'B'])); + expect($unserialized)->toEqual(new DataCollection(SimpleData::class, ['A', 'B'])); }); it('during the serialization process some properties are thrown away', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); $collection->include('test'); $collection->exclude('test'); diff --git a/tests/DataPipes/CastPropertiesDataPipeTest.php b/tests/DataPipes/CastPropertiesDataPipeTest.php index 656bbf1a..6f633a5e 100644 --- a/tests/DataPipes/CastPropertiesDataPipeTest.php +++ b/tests/DataPipes/CastPropertiesDataPipeTest.php @@ -4,6 +4,7 @@ use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Data; +use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; @@ -43,6 +44,13 @@ ['string' => 'you'], ['string' => 'up'], ], + 'nestedArray' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], ]); expect($data)->toBeInstanceOf(ComplicatedData::class) @@ -58,7 +66,14 @@ ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00')) ->explicitCast->toEqual(CarbonImmutable::createFromFormat('d-m-Y', '16-06-1994')) ->nestedData->toEqual(SimpleData::from('hello')) - ->nestedCollection->toEqual(SimpleData::collection([ + ->nestedCollection->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ], DataCollection::class)) + ->nestedArray->toEqual(SimpleData::collect([ SimpleData::from('never'), SimpleData::from('gonna'), SimpleData::from('give'), @@ -80,7 +95,10 @@ 'explicitCast' => DateTime::createFromFormat('d-m-Y', '16-06-1994'), 'defaultCast' => DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00'), 'nestedData' => SimpleData::from('hello'), - 'nestedCollection' => SimpleData::collection([ + 'nestedCollection' => SimpleData::collect([ + 'never', 'gonna', 'give', 'you', 'up', + ], DataCollection::class), + 'nestedArray' => SimpleData::collect([ 'never', 'gonna', 'give', 'you', 'up', ]), ]); @@ -97,7 +115,14 @@ ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00')) ->explicitCast->toEqual(DateTime::createFromFormat('d-m-Y', '16-06-1994')) ->nestedData->toEqual(SimpleData::from('hello')) - ->nestedCollection->toEqual(SimpleData::collection([ + ->nestedCollection->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ], DataCollection::class)) + ->nestedArray->toEqual(SimpleData::collect([ SimpleData::from('never'), SimpleData::from('gonna'), SimpleData::from('give'), @@ -132,32 +157,41 @@ 'models' => [['id' => 10], ['id' => 20],], ]); - expect($data)->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual(ModelData::collection([['id' => 10], ['id' => 20]])); + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); $data = NestedModelCollectionData::from([ 'models' => [new DummyModel(['id' => 10]), new DummyModel(['id' => 20]),], ]); - expect($data)->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual(ModelData::collection([['id' => 10], ['id' => 20]])); + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); $data = NestedModelCollectionData::from([ - 'models' => ModelData::collection([['id' => 10], ['id' => 20]]), + 'models' => ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class), ]); - expect($data)->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual(ModelData::collection([['id' => 10], ['id' => 20]])); + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); }); it('works nicely with lazy data', function () { $data = NestedLazyData::from([ - 'simple' => Lazy::create(fn () => SimpleData::from('Hello')), + 'simple' => Lazy::create(fn() => SimpleData::from('Hello')), ]); expect($data->simple) ->toBeInstanceOf(Lazy::class) - ->toEqual(Lazy::create(fn () => SimpleData::from('Hello'))); + ->toEqual(Lazy::create(fn() => SimpleData::from('Hello'))); }); it('allows casting', function () { diff --git a/tests/DataPipes/MapPropertiesDataPipeTest.php b/tests/DataPipes/MapPropertiesDataPipeTest.php index 5b1e6ed7..48561d5b 100644 --- a/tests/DataPipes/MapPropertiesDataPipeTest.php +++ b/tests/DataPipes/MapPropertiesDataPipeTest.php @@ -158,7 +158,7 @@ it('can map properties into data collections', function () { $dataClass = new class () extends Data { #[MapInputName('something'), DataCollectionOf(SimpleData::class)] - public DataCollection $mapped; + public array $mapped; }; $value = collect([ @@ -171,7 +171,7 @@ $data = $dataClass::from($value); expect($data->mapped)->toEqual( - SimpleData::collection([ + SimpleData::collect([ 'We are the knights who say, ni!', 'Bring us a, shrubbery!', ]) @@ -181,7 +181,7 @@ it('can map properties into data collections which map properties again', function () { $dataClass = new class () extends Data { #[MapInputName('something'), DataCollectionOf(SimpleDataWithMappedProperty::class)] - public DataCollection $mapped; + public array $mapped; }; $value = collect([ @@ -194,7 +194,7 @@ $data = $dataClass::from($value); expect($data->mapped)->toEqual( - SimpleDataWithMappedProperty::collection([ + SimpleDataWithMappedProperty::collect([ ['description' => 'We are the knights who say, ni!'], ['description' => 'Bring us a, shrubbery!'], ]) @@ -215,7 +215,7 @@ expect($data) ->casedProperty->toEqual('We are the knights who say, ni!') ->dataCasedProperty->toEqual(SimpleData::from('Bring us a, shrubbery!')) - ->dataCollectionCasedProperty->toEqual(SimpleData::collection([ + ->dataCollectionCasedProperty->toEqual(SimpleData::collect([ 'One that looks nice!', 'But not too expensive!', ])); diff --git a/tests/DataTest.php b/tests/DataTest.php index d0358ffa..688f9675 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -7,6 +7,7 @@ use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Illuminate\Support\Enumerable; use Inertia\LazyProp; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapOutputName; @@ -72,11 +73,11 @@ }); it('can create a collection of resources', function () { - $collection = SimpleData::collection(collect([ + $collection = SimpleData::collect(collect([ 'Ruben', 'Freek', 'Brent', - ])); + ]), DataCollection::class); expect($collection->toArray()) ->toMatchArray([ @@ -116,7 +117,7 @@ class TestIncludeableNestedLazyDataProperties extends Data public function __construct( public LazyData|Lazy $data, #[DataCollectionOf(LazyData::class)] - public DataCollection|Lazy $collection, + public array|Lazy $collection, ) { } } @@ -124,7 +125,7 @@ public function __construct( $data = new \TestIncludeableNestedLazyDataProperties( Lazy::create(fn() => LazyData::from('Hello')), - Lazy::create(fn() => LazyData::collection(['is', 'it', 'me', 'your', 'looking', 'for',])), + Lazy::create(fn() => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), ); expect((clone $data)->toArray())->toBe([]); @@ -169,12 +170,12 @@ class TestSpecificDefinedIncludeableCollectedAndNestedLazyData extends Data { public function __construct( #[DataCollectionOf(MultiLazyData::class)] - public DataCollection|Lazy $songs + public array|Lazy $songs ) { } } - $collection = Lazy::create(fn() => MultiLazyData::collection([ + $collection = Lazy::create(fn() => MultiLazyData::collect([ DummyDto::rick(), DummyDto::bon(), ])); @@ -327,7 +328,26 @@ public static function create(string $name): static }); it('can get the empty version of a data object', function () { - expect(EmptyData::empty())->toMatchArray([ + $dataClass = new class extends Data { + public string $property; + + public string|Lazy $lazyProperty; + + public array $array; + + public Collection $collection; + + #[DataCollectionOf(SimpleData::class)] + public DataCollection $dataCollection; + + public SimpleData $data; + + public Lazy|SimpleData $lazyData; + + public bool $defaultProperty = true; + }; + + expect($dataClass::empty())->toMatchArray([ 'property' => null, 'lazyProperty' => null, 'array' => [], @@ -548,7 +568,7 @@ public function __construct( it('can get the data object without transforming', function () { $data = new class ( $dataObject = new SimpleData('Test'), - $dataCollection = SimpleData::collection([new SimpleData('A'), new SimpleData('B')]), + $dataCollection = new DataCollection(SimpleData::class, ['A', 'B']), Lazy::create(fn() => new SimpleData('Lazy')), 'Test', $transformable = new DateTime('16 may 1994') @@ -698,7 +718,7 @@ public function __construct(public string $name) SimpleDataWithoutConstructor::fromString('World'), ]) ) - ->toEqual(SimpleDataWithoutConstructor::collection(['Hello', 'World'])); + ->toEqual(SimpleDataWithoutConstructor::collect(['Hello', 'World'], DataCollection::class)); }); it('can create a data object from a model', function () { @@ -928,7 +948,7 @@ public function __construct( } }; - $nestedDataCollection = $nestedData::collection([ + $nestedDataCollection = $nestedData::collect([ ['integer' => 314, 'string' => 'pi'], ['integer' => '69', 'string' => 'Laravel after hours'], ]); @@ -937,7 +957,7 @@ public function __construct( public function __construct( public Data $nestedData, #[DataCollectionOf(SimpleData::class)] - public DataCollection $nestedDataCollection, + public array $nestedDataCollection, ) { } }; @@ -950,7 +970,7 @@ public function __construct( WithTransformer(ConfidentialDataCollectionTransformer::class), DataCollectionOf(SimpleData::class) ] - public DataCollection $nestedDataCollection, + public array $nestedDataCollection, ) { } }; @@ -991,24 +1011,20 @@ public function __construct( }); it('can cast data object and collections using a custom cast', function () { - $dataWithDefaultCastsClass = new class (new SimpleData(''), SimpleData::collection([])) extends Data { - public function __construct( - public SimpleData $nestedData, - #[DataCollectionOf(SimpleData::class)] - public DataCollection $nestedDataCollection, - ) { - } + $dataWithDefaultCastsClass = new class () extends Data { + public SimpleData $nestedData; + + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection; }; - $dataWithCustomCastsClass = new class (new SimpleData(''), SimpleData::collection([])) extends Data { - public function __construct( - #[WithCast(ConfidentialDataCast::class)] - public SimpleData $nestedData, - #[WithCast(ConfidentialDataCollectionCast::class)] - #[DataCollectionOf(SimpleData::class)] - public DataCollection $nestedDataCollection, - ) { - } + $dataWithCustomCastsClass = new class () extends Data { + #[WithCast(ConfidentialDataCast::class)] + public SimpleData $nestedData; + + #[WithCast(ConfidentialDataCollectionCast::class)] + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection; }; $dataWithDefaultCasts = $dataWithDefaultCastsClass::from([ @@ -1024,12 +1040,12 @@ public function __construct( expect($dataWithDefaultCasts) ->nestedData->toEqual(SimpleData::from('a secret')) ->and($dataWithDefaultCasts) - ->nestedDataCollection->toEqual(SimpleData::collection(['another secret', 'yet another secret'])); + ->nestedDataCollection->toEqual(SimpleData::collect(['another secret', 'yet another secret'])); expect($dataWithCustomCasts) ->nestedData->toEqual(SimpleData::from('CONFIDENTIAL')) ->and($dataWithCustomCasts) - ->nestedDataCollection->toEqual(SimpleData::collection(['CONFIDENTIAL', 'CONFIDENTIAL'])); + ->nestedDataCollection->toEqual(SimpleData::collect(['CONFIDENTIAL', 'CONFIDENTIAL'])); }); it('can cast built-in types with custom casts', function () { @@ -1181,7 +1197,7 @@ public function __construct( it('can map transformed property names', function () { $data = new SimpleDataWithMappedProperty('hello'); - $dataCollection = SimpleDataWithMappedProperty::collection([ + $dataCollection = SimpleDataWithMappedProperty::collect([ ['description' => 'never'], ['description' => 'gonna'], ['description' => 'give'], @@ -1197,12 +1213,12 @@ public function __construct( #[MapOutputName('nested_other')] public SimpleDataWithMappedProperty $nested_renamed, #[DataCollectionOf(SimpleDataWithMappedProperty::class)] - public DataCollection $nested_collection, + public array $nested_collection, #[ MapOutputName('nested_other_collection'), DataCollectionOf(SimpleDataWithMappedProperty::class) ] - public DataCollection $nested_renamed_collection, + public array $nested_renamed_collection, ) { } }; @@ -1791,7 +1807,7 @@ public function __construct( )->toMatchArray(['wrap' => ['string' => 'Hello World']]); expect( - SimpleData::collection(['Hello', 'World']) + SimpleData::collect(['Hello', 'World'], DataCollection::class) ->wrap('wrap') ->toResponse(\request()) ->getData(true) @@ -1826,7 +1842,7 @@ public function __construct( ->toMatchArray(['string' => 'Hello World']); expect( - SimpleData::collection(['Hello', 'World']) + SimpleData::collect(['Hello', 'World'], DataCollection::class) ->toResponse(\request())->getData(true) ) ->toMatchArray([ @@ -1844,7 +1860,7 @@ public function __construct( ->toMatchArray(['string' => 'Hello World']); expect( - SimpleData::collection(['Hello', 'World']) + (new DataCollection(SimpleData::class, ['Hello', 'World'])) ->wrap('other-wrap') ->toResponse(\request()) ->getData(true) @@ -1857,7 +1873,7 @@ public function __construct( ]); expect( - SimpleData::collection(['Hello', 'World']) + (new DataCollection(SimpleData::class, ['Hello', 'World'])) ->withoutWrapping() ->toResponse(\request())->getData(true) ) @@ -1918,9 +1934,9 @@ public function with(): array it('wraps complex data structures', function () { $data = new MultiNestedData( new NestedData(SimpleData::from('Hello')), - NestedData::collection([ + [ new NestedData(SimpleData::from('World')), - ]), + ], ); expect( @@ -1940,9 +1956,9 @@ public function with(): array $data = new MultiNestedData( new NestedData(SimpleData::from('Hello')), - NestedData::collection([ + [ new NestedData(SimpleData::from('World')), - ]), + ], ); expect( @@ -1967,7 +1983,7 @@ public function with(): array ->toMatchArray(['string' => 'Hello World']); expect( - SimpleData::collection(['Hello', 'World'])->wrap('wrap') + SimpleData::collect(['Hello', 'World'], DataCollection::class)->wrap('wrap') ) ->toArray() ->toMatchArray([ @@ -2205,13 +2221,12 @@ public function __construct( expect($invaded->_wrap)->toBeNull(); }); - +// TODO: extend tests here it('can use an array to store data', function () { $dataClass = new class( [LazyData::from('A'), LazyData::from('B')], collect([LazyData::from('A'), LazyData::from('B')]), - ) extends Data - { + ) extends Data { public function __construct( #[DataCollectionOf(SimpleData::class)] public array $array, @@ -2236,4 +2251,40 @@ public function __construct( ['name' => 'B'], ], ]); -})->skip('TODO: yet to impelment'); +}); + +it('can write collection logic in a class', function () { + class TestSomeCustomCollection extends Collection + { + public function nameAll(): string + { + return $this->map(fn($data) => $data->string)->join(', '); + } + } + + $dataClass = new class() extends Data { + public string $string; + + public static function fromString(string $string): self + { + $s = new self(); + + $s->string = $string; + + return $s; + } + + public static function collectArray(array $items): \TestSomeCustomCollection + { + return new \TestSomeCustomCollection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(\TestSomeCustomCollection::class) + ->all()->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); +}); diff --git a/tests/Datasets/DataCollection.php b/tests/Datasets/DataCollection.php index 0007b3a7..d73141b1 100644 --- a/tests/Datasets/DataCollection.php +++ b/tests/Datasets/DataCollection.php @@ -1,18 +1,19 @@ [ - fn () => SimpleData::collection([ + fn () => SimpleData::collect([ 'A', 'B', SimpleData::from('C'), SimpleData::from('D'), - ]), + ], DataCollection::class), ]; yield "collection" => [ - fn () => SimpleData::collection([ + fn () => SimpleData::collect([ 'A', 'B', SimpleData::from('C'), SimpleData::from('D'), - ]), + ], DataCollection::class), ]; }); diff --git a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php index ee47261d..194cf962 100644 --- a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php @@ -9,8 +9,8 @@ class ConfidentialDataCollectionCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): DataCollection + public function cast(DataProperty $property, mixed $value, array $context): array { - return SimpleData::collection(array_map(fn () => SimpleData::from('CONFIDENTIAL'), $value)); + return array_map(fn () => SimpleData::from('CONFIDENTIAL'), $value); } } diff --git a/tests/Fakes/ComplicatedData.php b/tests/Fakes/ComplicatedData.php index a9dab0b6..12e6eeb4 100644 --- a/tests/Fakes/ComplicatedData.php +++ b/tests/Fakes/ComplicatedData.php @@ -4,6 +4,7 @@ use Carbon\CarbonImmutable; use DateTime; +use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Data; @@ -28,6 +29,8 @@ public function __construct( public SimpleData $nestedData, /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[] */ public DataCollection $nestedCollection, + #[DataCollectionOf(SimpleData::class)] + public array $nestedArray, ) { } } diff --git a/tests/Fakes/DataWithMapper.php b/tests/Fakes/DataWithMapper.php index dcec9361..73fcba60 100644 --- a/tests/Fakes/DataWithMapper.php +++ b/tests/Fakes/DataWithMapper.php @@ -16,5 +16,5 @@ class DataWithMapper extends Data public SimpleData $dataCasedProperty; #[DataCollectionOf(SimpleData::class)] - public DataCollection $dataCollectionCasedProperty; + public array $dataCollectionCasedProperty; } diff --git a/tests/Fakes/EmptyData.php b/tests/Fakes/EmptyData.php deleted file mode 100644 index a6fe43e5..00000000 --- a/tests/Fakes/EmptyData.php +++ /dev/null @@ -1,25 +0,0 @@ -toCollection()->map(fn (Data $data) => (new ConfidentialDataTransformer())->transform($property, $data))->toArray(); + /** @var array $value */ + return array_map(fn (Data $data) => (new ConfidentialDataTransformer())->transform($property, $data), $value); } } diff --git a/tests/RequestDataTest.php b/tests/RequestDataTest.php index 66208ae7..56881a02 100644 --- a/tests/RequestDataTest.php +++ b/tests/RequestDataTest.php @@ -7,6 +7,7 @@ use Illuminate\Testing\TestResponse; use Illuminate\Validation\ValidationException; +use Spatie\LaravelData\DataCollection; use function Pest\Laravel\handleExceptions; use function Pest\Laravel\postJson; @@ -149,10 +150,10 @@ public static function fromRequest(Request $request) it('can wrap data collections', function () { Route::post('/example-route', function () { - return SimpleData::collection([ + return SimpleData::collect([ request()->input('string'), strtoupper(request()->input('string')), - ])->wrap('data'); + ], DataCollection::class)->wrap('data'); }); performRequest('Hello World') diff --git a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php index 024c1d66..ff800176 100644 --- a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php +++ b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php @@ -28,7 +28,7 @@ $data = new class ( 'Hello', LazyData::from('Hello'), - LazyData::collection(['Hello', 'World']) + LazyData::collect(['Hello', 'World']) ) extends Data { public static ?array $allowedIncludes; @@ -36,7 +36,7 @@ public function __construct( public string $property, public LazyData $nested, #[DataCollectionOf(LazyData::class)] - public DataCollection $collection, + public array $collection, ) { } diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index a9dc34a0..dfe59a25 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -4,6 +4,7 @@ use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Factories\DataTypeFactory; +use Spatie\LaravelData\Support\Types\Type; it('can create a data parameter', function () { $class = new class ('', '', '') extends Data { @@ -17,42 +18,42 @@ public function __construct( }; $reflection = new ReflectionParameter([$class::class, '__construct'], 'nonPromoted'); - $parameter = DataParameter::create($reflection); + $parameter = DataParameter::create($reflection, $class::class); expect($parameter) ->name->toEqual('nonPromoted') ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(DataTypeFactory::create()->build($reflection)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); $reflection = new ReflectionParameter([$class::class, '__construct'], 'withoutType'); - $parameter = DataParameter::create($reflection); + $parameter = DataParameter::create($reflection, $class::class); expect($parameter) ->name->toEqual('withoutType') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(DataTypeFactory::create()->build($reflection)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); $reflection = new ReflectionParameter([$class::class, '__construct'], 'property'); - $parameter = DataParameter::create($reflection); + $parameter = DataParameter::create($reflection, $class::class); expect($parameter) ->name->toEqual('property') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(DataTypeFactory::create()->build($reflection)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); $reflection = new ReflectionParameter([$class::class, '__construct'], 'propertyWithDefault'); - $parameter = DataParameter::create($reflection); + $parameter = DataParameter::create($reflection, $class::class); expect($parameter) ->name->toEqual('propertyWithDefault') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeTrue() ->defaultValue->toEqual('hello') - ->type->toEqual(DataTypeFactory::create()->build($reflection)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); }); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index 6ecee392..b9cd9b77 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -914,15 +914,15 @@ function (object $class, string $type, ?string $expectedType) { */ class TestDataTypeWithClassAnnotatedProperty{ public function __construct( - public DataCollection $property, + public array $property, ) { } } - $type = resolveDataType(new \TestDataTypeWithClassAnnotatedProperty(SimpleData::collection([]))); + $type = resolveDataType(new \TestDataTypeWithClassAnnotatedProperty([])); expect($type) - ->kind->toBe(DataTypeKind::DataCollection) + ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class); + ->dataCollectableClass->toBe('array'); }); diff --git a/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php b/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php index 2d60c8bd..1e14b327 100644 --- a/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php +++ b/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php @@ -2,6 +2,7 @@ use Illuminate\Support\Facades\DB; +use Spatie\LaravelData\DataCollection; use function Pest\Laravel\assertDatabaseHas; use Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCasts; @@ -15,10 +16,10 @@ it('can save a data collection', function () { DummyModelWithCasts::create([ - 'data_collection' => SimpleData::collection([ - new SimpleData('Hello'), - new SimpleData('World'), - ]), + 'data_collection' => SimpleData::collect([ + 'Hello', + 'World', + ], DataCollection::class), ]); assertDatabaseHas(DummyModelWithCasts::class, [ @@ -56,7 +57,7 @@ /** @var \Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCasts $model */ $model = DummyModelWithCasts::first(); - expect($model->data_collection)->toEqual(SimpleData::collection([ + expect($model->data_collection)->toEqual(new DataCollection(SimpleData::class, [ new SimpleData('Hello'), new SimpleData('World'), ])); diff --git a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php index 0a37c7fd..b5bce5b1 100644 --- a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php +++ b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php @@ -12,6 +12,7 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer; +use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\SimpleData; use function Spatie\Snapshots\assertMatchesSnapshot as baseAssertMatchesSnapshot; @@ -28,7 +29,7 @@ function assertMatchesSnapshot($actual, Driver $driver = null): void it('can convert a data object to Typescript', function () { $config = TypeScriptTransformerConfig::create(); - $data = new class (null, Optional::create(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), SimpleData::from('Simple data'), SimpleData::collection([]), SimpleData::collection([]), SimpleData::collection([])) extends Data { + $data = new class (null, Optional::create(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), SimpleData::from('Simple data'), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, [])) extends Data { public function __construct( public null|int $nullable, public Optional|int $undefineable, @@ -61,7 +62,7 @@ public function __construct( it('uses the correct types for data collection of attributes', function () { $config = TypeScriptTransformerConfig::create(); - $collection = SimpleData::collection([]); + $collection = new DataCollection(SimpleData::class, []); $data = new class ($collection, $collection, $collection, $collection, $collection, $collection, $collection) extends Data { public function __construct( @@ -94,7 +95,7 @@ public function __construct( it('uses the correct types for paginated data collection for attributes ', function () { $config = TypeScriptTransformerConfig::create(); - $collection = SimpleData::collection(new LengthAwarePaginator([], 0, 15)); + $collection = new PaginatedDataCollection(SimpleData::class, new LengthAwarePaginator([], 0, 15)); $data = new class ($collection, $collection, $collection, $collection, $collection, $collection, $collection) extends Data { public function __construct( @@ -127,7 +128,7 @@ public function __construct( it('uses the correct types for cursor paginated data collection of attributes', function () { $config = TypeScriptTransformerConfig::create(); - $collection = SimpleData::collection(new CursorPaginator([], 15)); + $collection = new CursorPaginatedDataCollection(SimpleData::class, new CursorPaginator([], 15)); $data = new class ($collection, $collection, $collection, $collection, $collection, $collection, $collection) extends Data { public function __construct( From 7f3c82e543025da86328cec391fb2ebe05bc3f35 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 22 Feb 2023 11:31:46 +0100 Subject: [PATCH 007/124] wip --- .../DataCollectableFromSomethingResolver.php | 194 +++++------------- .../DataCollectables/ArrayDataCollectable.php | 16 -- .../DataCollectables/DataCollectable.php | 10 - .../DataCollectionDataCollectable.php | 21 -- 4 files changed, 56 insertions(+), 185 deletions(-) delete mode 100644 src/Support/DataCollectables/ArrayDataCollectable.php delete mode 100644 src/Support/DataCollectables/DataCollectable.php delete mode 100644 src/Support/DataCollectables/DataCollectionDataCollectable.php diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index a6275d87..b683d868 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -9,6 +9,7 @@ use Illuminate\Http\Request; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Spatie\LaravelData\Contracts\BaseData; @@ -22,6 +23,7 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Types\PartialType; +use function Pest\Laravel\instance; class DataCollectableFromSomethingResolver { @@ -62,37 +64,20 @@ public function execute( return $collectable; } - $dataTypeKind = $intoType->getDataTypeKind(); + $intoDataTypeKind = $intoType->getDataTypeKind(); - if ($dataTypeKind === DataTypeKind::Array) { - return $this->createArray($dataClass, $items); - } - - if ($dataTypeKind === DataTypeKind::Enumerable) { - return $this->createEnumerable($dataClass, $items, $intoType); - } - - if ($dataTypeKind === DataTypeKind::DataCollection) { - return $this->createDataCollection($dataClass, $items, $intoType); - } - - if ($dataTypeKind === DataTypeKind::Paginator) { - return $this->createPaginator($dataClass, $items, $intoType); - } - - if ($dataTypeKind === DataTypeKind::DataPaginatedCollection) { - return $this->createPaginatedDataCollection($dataClass, $items, $intoType); - } + $normalizedItems = $this->normalizeItems($items, $dataClass); - if ($dataTypeKind === DataTypeKind::CursorPaginator) { - return $this->createCursorPaginator($dataClass, $items, $intoType); - } - - if ($dataTypeKind === DataTypeKind::DataCursorPaginatedCollection) { - return $this->createCursorPaginatedDataCollection($dataClass, $items, $intoType); - } - - throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name); + return match ($intoDataTypeKind) { + DataTypeKind::Array => $this->normalizeToArray($normalizedItems), + DataTypeKind::Enumerable => new $intoType->name($this->normalizeToArray($normalizedItems)), + DataTypeKind::DataCollection => new $intoType->name($dataClass, $this->normalizeToArray($normalizedItems)), + DataTypeKind::DataPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToPaginator($normalizedItems)), + DataTypeKind::DataCursorPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems)), + DataTypeKind::Paginator => $this->normalizeToPaginator($normalizedItems), + DataTypeKind::CursorPaginator => $this->normalizeToCursorPaginator($normalizedItems), + default => CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name) + }; } protected function createFromCustomCreationMethod( @@ -130,146 +115,79 @@ protected function createFromCustomCreationMethod( return null; } - protected function createArray( + protected function normalizeItems( + mixed $items, string $dataClass, - mixed $items - ): array { - if ($items instanceof DataCollection) { + ): array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator { + if($items instanceof PaginatedDataCollection + || $items instanceof CursorPaginatedDataCollection + || $items instanceof DataCollection + ){ $items = $items->items(); } - if ($items instanceof Enumerable) { - $items = $items->all(); - } - - if (is_array($items)) { - return array_map( - $this->itemsToDataClosure($dataClass), - $items, - ); - } - - throw CannotCreateDataCollectable::create(get_debug_type($items), 'array'); - } - - protected function createEnumerable( - string $dataClass, - mixed $items, - PartialType $intoType, - ): Enumerable { - if ($items instanceof DataCollection) { + if ($items instanceof Paginator + || $items instanceof AbstractPaginator + || $items instanceof CursorPaginator + || $items instanceof AbstractCursorPaginator) { $items = $items->items(); } - if (is_array($items)) { - return new $intoType->name($items); - } - - if ($items instanceof Enumerable) { - return $items->map( - $this->itemsToDataClosure($dataClass) - ); - } - - throw CannotCreateDataCollectable::create(get_debug_type($items), Enumerable::class); - } - - protected function createDataCollection( - string $dataClass, - mixed $items, - PartialType $intoType, - ): DataCollection { if ($items instanceof Enumerable) { $items = $items->all(); } if (is_array($items)) { - return new $intoType->name($dataClass, $items); - } - - if ($items instanceof DataCollection) { - return $items->map( - $this->itemsToDataClosure($dataClass) + return array_map( + $this->itemsToDataClosure($dataClass), + $items ); } - throw CannotCreateDataCollectable::create(get_debug_type($items), DataCollection::class); + throw new Exception('Unable to normalize items'); } - protected function createPaginator( - string $dataClass, - mixed $items, - PartialType $intoType, - ): AbstractPaginator|Paginator { - if ($items instanceof PaginatedDataCollection) { - $items = $items->items(); - } - - if ($items instanceof AbstractPaginator || $items instanceof Paginator) { - return $items->through( - $this->itemsToDataClosure($dataClass) - ); - } - - throw CannotCreateDataCollectable::create(get_debug_type($items), Paginator::class); + protected function normalizeToArray( + array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator $items, + ): array { + return is_array($items) + ? $items + : $items->items(); } - protected function createPaginatedDataCollection( - string $dataClass, - mixed $items, - PartialType $intoType, - ): PaginatedDataCollection { - if ($items instanceof AbstractPaginator || $items instanceof Paginator) { - return new $intoType->name($dataClass, $items); + protected function normalizeToPaginator( + array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator $items, + ): Paginator|AbstractPaginator { + if ($items instanceof Paginator || $items instanceof AbstractPaginator) { + return $items; } - if ($items instanceof PaginatedDataCollection) { - return $items->through( - $this->itemsToDataClosure($dataClass) - ); - } + $normalizedItems = $this->normalizeToArray($items); - throw CannotCreateDataCollectable::create(get_debug_type($items), PaginatedDataCollection::class); + return new LengthAwarePaginator( + $normalizedItems, + count($normalizedItems), + $items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator ? $items->perPage() : 15, + ); } - protected function createCursorPaginator( - string $dataClass, - mixed $items, - PartialType $intoType, + protected function normalizeToCursorPaginator( + array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator $items, ): CursorPaginator|AbstractCursorPaginator { - if ($items instanceof CursorPaginatedDataCollection) { - $items = $items->items(); - } - - if ($items instanceof AbstractCursorPaginator || $items instanceof CursorPaginator) { - return $items->through( - $this->itemsToDataClosure($dataClass) - ); + if ($items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator) { + return $items; } - throw CannotCreateDataCollectable::create(get_debug_type($items), CursorPaginator::class); - } - - protected function createCursorPaginatedDataCollection( - string $dataClass, - mixed $items, - PartialType $intoType, - ): CursorPaginatedDataCollection { - if ($items instanceof AbstractCursorPaginator || $items instanceof CursorPaginator) { - return new $intoType->name($dataClass, $items); - } - - if ($items instanceof CursorPaginatedDataCollection) { - return $items->through( - $this->itemsToDataClosure($dataClass) - ); - } + $normalizedItems = $this->normalizeToArray($items); - throw CannotCreateDataCollectable::create(get_debug_type($items), CursorPaginatedDataCollection::class); + return new \Illuminate\Pagination\CursorPaginator( + $normalizedItems, + $items instanceof Paginator || $items instanceof AbstractPaginator ? $items->perPage() : 15, + ); } protected function itemsToDataClosure(string $dataClass): Closure { - return fn(mixed $data) => $dataClass::from($data); + return fn(mixed $data) => $data instanceof $dataClass ? $data : $dataClass::from($data); } } diff --git a/src/Support/DataCollectables/ArrayDataCollectable.php b/src/Support/DataCollectables/ArrayDataCollectable.php deleted file mode 100644 index f832e76c..00000000 --- a/src/Support/DataCollectables/ArrayDataCollectable.php +++ /dev/null @@ -1,16 +0,0 @@ -items(); - } - - public function denormalize(array $items, string $dataClass, string $collectableClass): DataCollection - { - return new $collectableClass($dataClass, $items); - } -} From 44000c68c19f8fc8782bd33a78cab2b070349676 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 22 Feb 2023 11:55:35 +0100 Subject: [PATCH 008/124] wip --- src/Concerns/BaseData.php | 17 ------------- src/Concerns/BaseDataCollectable.php | 17 ------------- src/Concerns/ContextableData.php | 35 +++++++++++++++++++++++++++ src/Concerns/DataTrait.php | 15 ------------ src/Concerns/WrappableData.php | 14 +---------- src/Contracts/BaseData.php | 4 +-- src/CursorPaginatedDataCollection.php | 2 ++ src/Data.php | 19 ++++++++++++++- src/DataCollection.php | 2 ++ src/Dto.php | 2 ++ src/PaginatedDataCollection.php | 2 ++ src/Resource.php | 2 ++ tests/DataCollectionTest.php | 1 - tests/DataTest.php | 19 +++++++++++++-- 14 files changed, 83 insertions(+), 68 deletions(-) create mode 100644 src/Concerns/ContextableData.php delete mode 100644 src/Concerns/DataTrait.php diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index eac2d00e..32372beb 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -118,21 +118,4 @@ public function __sleep(): array ->push('_additional') ->toArray(); } - - public function getDataContext(): DataContext - { - if ($this->_dataContext === null) { - return $this->_dataContext = new DataContext( - new PartialsDefinition( - $this instanceof IncludeableDataContract ? $this->includeProperties() : [], - $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], - $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], - $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], - ), - $this instanceof WrappableDataContract ? $this->getWrap() : new Wrap(WrapType::UseGlobal), - ); - } - - return $this->_dataContext; - } } diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 2d2384e6..48067c0f 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -49,21 +49,4 @@ public function __sleep(): array { return ['items', 'dataClass']; } - - public function getDataContext(): DataContext - { - if ($this->_dataContext === null) { - return $this->_dataContext = new DataContext( - new PartialsDefinition( - $this instanceof IncludeableDataContract ? $this->includeProperties() : [], - $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], - $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], - $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], - ), - $this instanceof WrappableDataContract ? $this->getWrap() : new Wrap(WrapType::UseGlobal), - ); - } - - return $this->_dataContext; - } } diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php new file mode 100644 index 00000000..36829a82 --- /dev/null +++ b/src/Concerns/ContextableData.php @@ -0,0 +1,35 @@ +_dataContext === null) { + $wrap = match (true){ + method_exists($this, 'defaultWrap') => new Wrap(WrapType::Defined, $this->defaultWrap()), + default => new Wrap(WrapType::UseGlobal), + }; + + return $this->_dataContext = new DataContext( + new PartialsDefinition( + $this instanceof IncludeableDataContract ? $this->includeProperties() : [], + $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], + $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], + $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], + ), + $this instanceof WrappableDataContract ? $wrap : new Wrap(WrapType::UseGlobal), + ); + } + + return $this->_dataContext; + } +} diff --git a/src/Concerns/DataTrait.php b/src/Concerns/DataTrait.php deleted file mode 100644 index fdfd08a8..00000000 --- a/src/Concerns/DataTrait.php +++ /dev/null @@ -1,15 +0,0 @@ -getDataContext()->wrap = new Wrap(WrapType::Disabled); - $this->_wrap = new Wrap(WrapType::Disabled); return $this; } @@ -20,21 +17,12 @@ public function withoutWrapping(): static public function wrap(string $key): static { $this->getDataContext()->wrap = new Wrap(WrapType::Defined, $key); - $this->_wrap = new Wrap(WrapType::Defined, $key); return $this; } public function getWrap(): Wrap { - if ($this->_wrap) { - return $this->_wrap; - } - - if (method_exists($this, 'defaultWrap')) { - return new Wrap(WrapType::Defined, $this->defaultWrap()); - } - - return $this->_wrap ?? new Wrap(WrapType::UseGlobal); + return $this->getDataContext()->wrap ?? new Wrap(WrapType::UseGlobal); } } diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 01de6865..6e1c3e29 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -30,9 +30,9 @@ public static function withoutMagicalCreationCollect(mixed $items, ?string $into public static function normalizers(): array; - public static function pipeline(): DataPipeline; - public static function prepareForPipeline(\Illuminate\Support\Collection $properties): \Illuminate\Support\Collection; + public static function pipeline(): DataPipeline; + public function getDataContext(): DataContext; } diff --git a/src/CursorPaginatedDataCollection.php b/src/CursorPaginatedDataCollection.php index dfe4d135..cb8f85d1 100644 --- a/src/CursorPaginatedDataCollection.php +++ b/src/CursorPaginatedDataCollection.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Pagination\CursorPaginator; use Spatie\LaravelData\Concerns\BaseDataCollectable; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; use Spatie\LaravelData\Concerns\TransformableData; @@ -29,6 +30,7 @@ class CursorPaginatedDataCollection implements DataCollectable /** @use \Spatie\LaravelData\Concerns\BaseDataCollectable */ use BaseDataCollectable; + use ContextableData; /** @var CursorPaginator */ protected CursorPaginator $items; diff --git a/src/Data.php b/src/Data.php index f0cee085..8e8e513b 100644 --- a/src/Data.php +++ b/src/Data.php @@ -2,10 +2,27 @@ namespace Spatie\LaravelData; +use Spatie\LaravelData\Concerns\AppendableData; +use Spatie\LaravelData\Concerns\BaseData; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\DataTrait; +use Spatie\LaravelData\Concerns\EmptyData; +use Spatie\LaravelData\Concerns\IncludeableData; +use Spatie\LaravelData\Concerns\ResponsableData; +use Spatie\LaravelData\Concerns\TransformableData; +use Spatie\LaravelData\Concerns\ValidateableData; +use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\DataObject; abstract class Data implements DataObject { - use DataTrait; + use ResponsableData; + use IncludeableData; + use AppendableData; + use ValidateableData; + use WrappableData; + use TransformableData; + use BaseData; + use EmptyData; + use ContextableData; } diff --git a/src/DataCollection.php b/src/DataCollection.php index 6a467b5d..60ec33f5 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -6,6 +6,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Spatie\LaravelData\Concerns\BaseDataCollectable; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\EnumerableMethods; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; @@ -32,6 +33,7 @@ class DataCollection implements DataCollectable, ArrayAccess use IncludeableData; use WrappableData; use TransformableData; + use ContextableData; /** @use \Spatie\LaravelData\Concerns\EnumerableMethods */ use EnumerableMethods; diff --git a/src/Dto.php b/src/Dto.php index 3e566fc5..fa5a6f5e 100644 --- a/src/Dto.php +++ b/src/Dto.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData; use Spatie\LaravelData\Concerns\BaseData; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; @@ -11,4 +12,5 @@ class Dto implements ValidateableDataContract, BaseDataContract { use ValidateableData; use BaseData; + use ContextableData; } diff --git a/src/PaginatedDataCollection.php b/src/PaginatedDataCollection.php index b022aae8..5d7434b0 100644 --- a/src/PaginatedDataCollection.php +++ b/src/PaginatedDataCollection.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Contracts\Pagination\Paginator; use Spatie\LaravelData\Concerns\BaseDataCollectable; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; use Spatie\LaravelData\Concerns\TransformableData; @@ -29,6 +30,7 @@ class PaginatedDataCollection implements DataCollectable /** @use \Spatie\LaravelData\Concerns\BaseDataCollectable */ use BaseDataCollectable; + use ContextableData; protected Paginator $items; diff --git a/src/Resource.php b/src/Resource.php index d87a20d1..5358c12d 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -4,6 +4,7 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; @@ -26,4 +27,5 @@ class Resource implements BaseDataContract, AppendableDataContract, IncludeableD use TransformableData; use WrappableData; use EmptyData; + use ContextableData; } diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 7e0497fa..89968177 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -514,5 +514,4 @@ function (string $operation, array $arguments, array $expected) { $invaded = invade($unserialized); expect($invaded->_dataContext)->toBeNull(); - expect($invaded->_wrap)->toBeNull(); }); diff --git a/tests/DataTest.php b/tests/DataTest.php index 688f9675..e7779b30 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -14,8 +14,16 @@ use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Attributes\WithTransformer; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; +use Spatie\LaravelData\Concerns\AppendableData; +use Spatie\LaravelData\Concerns\BaseData; +use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\DataTrait; +use Spatie\LaravelData\Concerns\IncludeableData; +use Spatie\LaravelData\Concerns\ResponsableData; +use Spatie\LaravelData\Concerns\TransformableData; +use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Concerns\WireableData; +use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; @@ -2051,7 +2059,15 @@ public function with(): array it('can use a trait', function () { $data = new class ('') implements DataObject { - use DataTrait; + use ResponsableData; + use IncludeableData; + use AppendableData; + use ValidateableData; + use WrappableData; + use TransformableData; + use BaseData; + use \Spatie\LaravelData\Concerns\EmptyData; + use ContextableData; public function __construct(public string $string) { @@ -2218,7 +2234,6 @@ public function __construct( $invaded = invade($unserialized); expect($invaded->_dataContext)->toBeNull(); - expect($invaded->_wrap)->toBeNull(); }); // TODO: extend tests here From c873e890b0a88252d29488f49ba13144b238729d Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 22 Feb 2023 13:21:30 +0100 Subject: [PATCH 009/124] wip --- src/DataPipes/CastPropertiesDataPipe.php | 15 ++++-- .../DataCollectableFromSomethingResolver.php | 22 ++++++--- src/Support/Creation/CollectableMetaData.php | 49 +++++++++++++++++++ .../Transforming/TransformationContext.php | 24 --------- 4 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 src/Support/Creation/CollectableMetaData.php delete mode 100644 src/Support/Transforming/TransformationContext.php diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index bd876cfc..faff8943 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\DataPipes; use Illuminate\Support\Collection; +use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; @@ -62,7 +63,7 @@ protected function cast( return $property->type->dataClass::from($value); } - if ($property->type->kind->isDataCollectable()) { + if($property->type->kind->isDataCollectable()){ return $property->type->dataClass::collect($value, $property->type->dataCollectableClass); } @@ -71,8 +72,14 @@ protected function cast( protected function shouldBeCasted(DataProperty $property, mixed $value): bool { - return gettype($value) === 'object' - ? ! $property->type->type->acceptsValue($value) - : true; + if (gettype($value) !== 'object') { + return true; + } + + if ($property->type->kind->isDataCollectable()) { + return true; // Transform everything to data objects + } + + return $property->type->type->acceptsValue($value) === false; } } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index b683d868..3be4fbc0 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -20,6 +20,7 @@ use Spatie\LaravelData\Exceptions\CannotCreateDataCollectable; use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Creation\CollectableMetaData; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Types\PartialType; @@ -66,16 +67,18 @@ public function execute( $intoDataTypeKind = $intoType->getDataTypeKind(); + $collectableMetaData = CollectableMetaData::fromOther($items); + $normalizedItems = $this->normalizeItems($items, $dataClass); return match ($intoDataTypeKind) { DataTypeKind::Array => $this->normalizeToArray($normalizedItems), DataTypeKind::Enumerable => new $intoType->name($this->normalizeToArray($normalizedItems)), DataTypeKind::DataCollection => new $intoType->name($dataClass, $this->normalizeToArray($normalizedItems)), - DataTypeKind::DataPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToPaginator($normalizedItems)), - DataTypeKind::DataCursorPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems)), - DataTypeKind::Paginator => $this->normalizeToPaginator($normalizedItems), - DataTypeKind::CursorPaginator => $this->normalizeToCursorPaginator($normalizedItems), + DataTypeKind::DataPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::DataCursorPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::Paginator => $this->normalizeToPaginator($normalizedItems, $collectableMetaData), + DataTypeKind::CursorPaginator => $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData), default => CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name) }; } @@ -130,7 +133,7 @@ protected function normalizeItems( || $items instanceof AbstractPaginator || $items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator) { - $items = $items->items(); + return $items->through($this->itemsToDataClosure($dataClass)); } if ($items instanceof Enumerable) { @@ -157,6 +160,7 @@ protected function normalizeToArray( protected function normalizeToPaginator( array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator $items, + CollectableMetaData $collectableMetaData, ): Paginator|AbstractPaginator { if ($items instanceof Paginator || $items instanceof AbstractPaginator) { return $items; @@ -166,13 +170,14 @@ protected function normalizeToPaginator( return new LengthAwarePaginator( $normalizedItems, - count($normalizedItems), - $items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator ? $items->perPage() : 15, + $collectableMetaData->paginator_total ?? count($items), + $collectableMetaData->paginator_per_page ?? 15, ); } protected function normalizeToCursorPaginator( array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator $items, + CollectableMetaData $collectableMetaData, ): CursorPaginator|AbstractCursorPaginator { if ($items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator) { return $items; @@ -182,7 +187,8 @@ protected function normalizeToCursorPaginator( return new \Illuminate\Pagination\CursorPaginator( $normalizedItems, - $items instanceof Paginator || $items instanceof AbstractPaginator ? $items->perPage() : 15, + $collectableMetaData->paginator_per_page ?? 15, + $collectableMetaData->paginator_cursor ); } diff --git a/src/Support/Creation/CollectableMetaData.php b/src/Support/Creation/CollectableMetaData.php new file mode 100644 index 00000000..afb6a59c --- /dev/null +++ b/src/Support/Creation/CollectableMetaData.php @@ -0,0 +1,49 @@ +total(), + paginator_page: $items->currentPage(), + paginator_per_page: $items->perPage(), + ); + } + + if ($items instanceof Paginator || $items instanceof AbstractPaginator) { + return new self( + paginator_page: $items->currentPage(), + paginator_per_page: $items->perPage() + ); + } + + if ($items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator) { + return new self( + paginator_per_page: $items->perPage(), + paginator_cursor: $items->cursor() + ); + } + + return new self(); + } +} diff --git a/src/Support/Transforming/TransformationContext.php b/src/Support/Transforming/TransformationContext.php deleted file mode 100644 index af946957..00000000 --- a/src/Support/Transforming/TransformationContext.php +++ /dev/null @@ -1,24 +0,0 @@ - Date: Wed, 22 Feb 2023 13:55:47 +0100 Subject: [PATCH 010/124] wip --- CHANGELOG.md | 11 +++++++++++ src/Concerns/DefaultableData.php | 11 +++++++++++ src/Contracts/DataObject.php | 2 +- src/Contracts/DefaultableData.php | 8 ++++++++ src/Data.php | 2 ++ src/DataPipes/DefaultValuesDataPipe.php | 14 ++++++++++++-- src/Dto.php | 5 ++++- src/Resource.php | 5 ++++- src/Support/DataClass.php | 3 +++ tests/DataPipes/DefaultValuesDataPipeTest.php | 19 +++++++++++++++++++ tests/DataTest.php | 2 ++ tests/Support/DataTypeTest.php | 2 ++ 12 files changed, 79 insertions(+), 5 deletions(-) create mode 100644 src/Concerns/DefaultableData.php create mode 100644 src/Contracts/DefaultableData.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 21dac5a3..dc994e5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to `laravel-data` will be documented in this file. +## 4.0.0 + +- Allow arrays, Collections, Paginators, ... to be used as DataCollections +- Add support for magically creating data collections +- Rewritten transformation system with respect to includeable properties +- Addition of collect method +- Removal of collection method +- Add support for using Laravel Model attributes as data properties +- Add support for class defined defaults +- Allow creating data objects using `from` without parameters + ## 3.1.0 - 2023-02-10 - Allow filling props from route parameters (#341) diff --git a/src/Concerns/DefaultableData.php b/src/Concerns/DefaultableData.php new file mode 100644 index 00000000..3eea8915 --- /dev/null +++ b/src/Concerns/DefaultableData.php @@ -0,0 +1,11 @@ +defaultable + ? app()->call([$class->name, 'defaults']) + : []; + $class ->properties - ->filter(fn (DataProperty $property) => ! $properties->has($property->name)) - ->each(function (DataProperty $property) use (&$properties) { + ->filter(fn(DataProperty $property) => ! $properties->has($property->name)) + ->each(function (DataProperty $property) use ($dataDefaults, &$properties) { + if (array_key_exists($property->name, $dataDefaults)) { + $properties[$property->name] = $dataDefaults[$property->name]; + + return; + } + if ($property->hasDefaultValue) { $properties[$property->name] = $property->defaultValue; diff --git a/src/Dto.php b/src/Dto.php index fa5a6f5e..06f8b9bf 100644 --- a/src/Dto.php +++ b/src/Dto.php @@ -4,13 +4,16 @@ use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; +use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; +use Spatie\LaravelData\Contracts\DefaultableData as DefaultDataContract; use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; -class Dto implements ValidateableDataContract, BaseDataContract +class Dto implements ValidateableDataContract, BaseDataContract, DefaultDataContract { use ValidateableData; use BaseData; use ContextableData; + use DefaultableData; } diff --git a/src/Resource.php b/src/Resource.php index 5358c12d..314a4985 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -5,6 +5,7 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; +use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; @@ -12,13 +13,14 @@ use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\AppendableData as AppendableDataContract; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; +use Spatie\LaravelData\Contracts\DefaultableData as DefaultDataContract; use Spatie\LaravelData\Contracts\EmptyData as EmptyDataContract; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract +class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract, DefaultDataContract { use BaseData; use AppendableData; @@ -28,4 +30,5 @@ class Resource implements BaseDataContract, AppendableDataContract, IncludeableD use WrappableData; use EmptyData; use ContextableData; + use DefaultableData; } diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 744ec672..90830f6e 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -10,6 +10,7 @@ use ReflectionProperty; use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\ResponsableData; use Spatie\LaravelData\Contracts\TransformableData; @@ -39,6 +40,7 @@ public function __construct( public readonly bool $responsable, public readonly bool $transformable, public readonly bool $validateable, + public readonly bool $defaultable, public readonly bool $wrappable, public readonly Collection $attributes, public readonly array $dataCollectablePropertyAnnotations @@ -75,6 +77,7 @@ public static function create(ReflectionClass $class): self responsable: $class->implementsInterface(ResponsableData::class), transformable: $class->implementsInterface(TransformableData::class), validateable: $class->implementsInterface(ValidateableData::class), + defaultable: $class->implementsInterface(DefaultableData::class), wrappable: $class->implementsInterface(WrappableData::class), attributes: $attributes, dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations diff --git a/tests/DataPipes/DefaultValuesDataPipeTest.php b/tests/DataPipes/DefaultValuesDataPipeTest.php index 45fac16a..83b2bf06 100644 --- a/tests/DataPipes/DefaultValuesDataPipeTest.php +++ b/tests/DataPipes/DefaultValuesDataPipeTest.php @@ -16,3 +16,22 @@ public function __construct( expect(new $dataClass(null, new Optional(), 'Hi')) ->toEqual($dataClass::from([])); }); + +it('can create a data object with defined defaults', function () { + $dataClass = new class ('', '', '') extends Data { + public function __construct( + public string $stringWithDefault, + ) { + } + + public static function defaults(): array + { + return [ + 'stringWithDefault' => 'Hi', + ]; + } + }; + + expect(new $dataClass('Hi'))->toEqual($dataClass::from([])); +}); + diff --git a/tests/DataTest.php b/tests/DataTest.php index e7779b30..fd1df735 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -18,6 +18,7 @@ use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\DataTrait; +use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; use Spatie\LaravelData\Concerns\TransformableData; @@ -2068,6 +2069,7 @@ public function with(): array use BaseData; use \Spatie\LaravelData\Concerns\EmptyData; use ContextableData; + use DefaultableData; public function __construct(public string $string) { diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index b9cd9b77..58edbf4b 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -12,6 +12,7 @@ use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\PrepareableData; @@ -595,6 +596,7 @@ function (object $class, array $expected) { DataObject::class, AppendableData::class, BaseData::class, + DefaultableData::class, IncludeableData::class, ResponsableData::class, TransformableData::class, From fb9125ac4acc16901ef9bfbf3d3ae16a1e9b6dff Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 22 Feb 2023 12:56:24 +0000 Subject: [PATCH 011/124] Fix styling --- src/Concerns/BaseData.php | 13 +-- src/Concerns/BaseDataCollectable.php | 11 --- src/Concerns/ContextableData.php | 2 +- src/Concerns/EmptyData.php | 29 ------ src/Concerns/IncludeableData.php | 3 - src/Concerns/ResponsableData.php | 8 +- src/Contracts/EmptyData.php | 11 --- src/Contracts/IncludeableData.php | 1 - src/Contracts/TransformableData.php | 1 - src/Data.php | 1 - src/DataPipes/CastPropertiesDataPipe.php | 7 +- src/DataPipes/DefaultValuesDataPipe.php | 2 +- .../DataCollectableFromSomethingResolver.php | 11 +-- .../PartialsTreeFromRequestResolver.php | 2 - .../TransformedDataCollectionResolver.php | 19 +--- src/Resolvers/TransformedDataResolver.php | 4 - .../Annotations/DataCollectableAnnotation.php | 3 +- .../DataCollectableAnnotationReader.php | 4 +- src/Support/DataClass.php | 20 ++-- src/Support/DataMethod.php | 15 ++- src/Support/DataProperty.php | 1 - src/Support/DataType.php | 7 -- src/Support/Factories/DataTypeFactory.php | 17 +--- .../Partials/ForwardsToPartialsDefinition.php | 2 +- src/Support/Partials/PartialsDefinition.php | 3 +- src/Support/Transformation/DataContext.php | 1 - .../PartialTransformationContext.php | 5 +- .../Transformation/TransformationContext.php | 8 -- .../TransformationContextFactory.php | 7 -- .../DataTypeScriptTransformer.php | 1 - src/Support/Types/IntersectionType.php | 3 - src/Support/Types/MultiType.php | 5 +- src/Support/Types/Type.php | 8 +- src/Support/Types/UnionType.php | 2 - tests/DataCollectionTest.php | 14 +-- .../DataPipes/CastPropertiesDataPipeTest.php | 4 +- tests/DataPipes/DefaultValuesDataPipeTest.php | 1 - tests/DataPipes/MapPropertiesDataPipeTest.php | 1 - tests/DataTest.php | 93 +++++++++---------- .../Casts/ConfidentialDataCollectionCast.php | 1 - tests/Fakes/CollectionAnnotationsData.php | 7 +- tests/Fakes/DataWithMapper.php | 1 - tests/Fakes/FakeModelData.php | 1 - tests/Fakes/Models/FakeModel.php | 2 +- tests/Fakes/MultiNestedData.php | 1 - tests/Normalizers/ModelNormalizerTest.php | 4 - tests/RequestDataTest.php | 3 +- tests/Resolvers/EmptyDataResolverTest.php | 2 +- .../PartialsTreeFromRequestResolverTest.php | 1 - .../DataCollectableAnnotationReaderTest.php | 2 - tests/Support/DataMethodTest.php | 2 - tests/Support/DataParameterTest.php | 2 - tests/Support/DataTypeTest.php | 7 +- .../DataCollectionEloquentCastTest.php | 3 +- .../DataTypeScriptTransformerTest.php | 1 - 55 files changed, 102 insertions(+), 288 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 32372beb..8b6e66f7 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -8,8 +8,6 @@ use Illuminate\Pagination\AbstractPaginator; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; -use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; -use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; @@ -22,18 +20,9 @@ use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; -use Spatie\LaravelData\Resolvers\EmptyDataResolver; -use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Transformation\DataContext; -use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; -use Spatie\LaravelData\Support\Transformation\TransformationContext; -use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Support\Wrapping\Wrap; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use Spatie\LaravelData\Support\Wrapping\WrapType; trait BaseData { @@ -114,7 +103,7 @@ public function __sleep(): array { return app(DataConfig::class)->getDataClass(static::class) ->properties - ->map(fn(DataProperty $property) => $property->name) + ->map(fn (DataProperty $property) => $property->name) ->push('_additional') ->toArray(); } diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 48067c0f..4b90d5db 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -3,19 +3,8 @@ namespace Spatie\LaravelData\Concerns; use ArrayIterator; -use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; -use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; -use Spatie\LaravelData\Resolvers\TransformedDataResolver; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Transformation\DataContext; -use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; -use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Support\Wrapping\Wrap; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use Spatie\LaravelData\Support\Wrapping\WrapType; -use Spatie\LaravelData\Transformers\DataCollectableTransformer; /** * @template TKey of array-key diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php index 36829a82..e79ff710 100644 --- a/src/Concerns/ContextableData.php +++ b/src/Concerns/ContextableData.php @@ -14,7 +14,7 @@ trait ContextableData public function getDataContext(): DataContext { if ($this->_dataContext === null) { - $wrap = match (true){ + $wrap = match (true) { method_exists($this, 'defaultWrap') => new Wrap(WrapType::Defined, $this->defaultWrap()), default => new Wrap(WrapType::UseGlobal), }; diff --git a/src/Concerns/EmptyData.php b/src/Concerns/EmptyData.php index c2f006ef..6b809322 100644 --- a/src/Concerns/EmptyData.php +++ b/src/Concerns/EmptyData.php @@ -2,36 +2,7 @@ namespace Spatie\LaravelData\Concerns; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Contracts\Pagination\Paginator; -use Illuminate\Pagination\AbstractCursorPaginator; -use Illuminate\Pagination\AbstractPaginator; -use Illuminate\Support\Collection; -use Illuminate\Support\Enumerable; -use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; -use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -use Spatie\LaravelData\CursorPaginatedDataCollection; -use Spatie\LaravelData\DataCollection; -use Spatie\LaravelData\DataPipeline; -use Spatie\LaravelData\DataPipes\AuthorizedDataPipe; -use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe; -use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; -use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Resolvers\EmptyDataResolver; -use Spatie\LaravelData\Resolvers\TransformedDataResolver; -use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Transformation\DataContext; -use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; -use Spatie\LaravelData\Support\Transformation\TransformationContext; -use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Support\Wrapping\Wrap; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use Spatie\LaravelData\Support\Wrapping\WrapType; trait EmptyData { diff --git a/src/Concerns/IncludeableData.php b/src/Concerns/IncludeableData.php index 3b3b8738..36301b6f 100644 --- a/src/Concerns/IncludeableData.php +++ b/src/Concerns/IncludeableData.php @@ -2,11 +2,8 @@ namespace Spatie\LaravelData\Concerns; -use Closure; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; use Spatie\LaravelData\Support\Partials\PartialsDefinition; -use Spatie\LaravelData\Support\PartialsParser; -use Spatie\LaravelData\Support\PartialTrees; trait IncludeableData { diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 894bf8f5..05d16de5 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -8,9 +8,7 @@ use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Resolvers\PartialsTreeFromRequestResolver; use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; -use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; trait ResponsableData @@ -25,9 +23,11 @@ public function toResponse($request) $context = TransformationContextFactory::create() ->wrapExecutionType(WrapExecutionType::Enabled) ->get($this) - ->mergePartials(PartialTransformationContext::create( + ->mergePartials( + PartialTransformationContext::create( $this, - $this->getDataContext()->partialsDefinition) + $this->getDataContext()->partialsDefinition + ) ); $context = $this instanceof IncludeableDataContract diff --git a/src/Contracts/EmptyData.php b/src/Contracts/EmptyData.php index 3ec4f798..21a8a6a8 100644 --- a/src/Contracts/EmptyData.php +++ b/src/Contracts/EmptyData.php @@ -2,17 +2,6 @@ namespace Spatie\LaravelData\Contracts; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Contracts\Pagination\Paginator; -use Illuminate\Pagination\AbstractCursorPaginator; -use Illuminate\Pagination\AbstractPaginator; -use Illuminate\Support\Enumerable; -use Spatie\LaravelData\CursorPaginatedDataCollection; -use Spatie\LaravelData\DataCollection; -use Spatie\LaravelData\DataPipeline; -use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Transformation\DataContext; - interface EmptyData { public static function empty(array $extra = []): array; diff --git a/src/Contracts/IncludeableData.php b/src/Contracts/IncludeableData.php index bc665a28..c16604fe 100644 --- a/src/Contracts/IncludeableData.php +++ b/src/Contracts/IncludeableData.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Contracts; use Closure; -use Spatie\LaravelData\Support\PartialTrees; interface IncludeableData { diff --git a/src/Contracts/TransformableData.php b/src/Contracts/TransformableData.php index 81dbc212..97c0d825 100644 --- a/src/Contracts/TransformableData.php +++ b/src/Contracts/TransformableData.php @@ -8,7 +8,6 @@ use JsonSerializable; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; interface TransformableData extends JsonSerializable, Jsonable, Arrayable, EloquentCastable { diff --git a/src/Data.php b/src/Data.php index 21b6d317..4476c826 100644 --- a/src/Data.php +++ b/src/Data.php @@ -5,7 +5,6 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; -use Spatie\LaravelData\Concerns\DataTrait; use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index faff8943..f8fed129 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -3,11 +3,8 @@ namespace Spatie\LaravelData\DataPipes; use Illuminate\Support\Collection; -use Spatie\LaravelData\Casts\Cast; -use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; -use Spatie\LaravelData\Resolvers\DataCollectableResolver; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; @@ -24,7 +21,7 @@ public function handle(mixed $payload, DataClass $class, Collection $properties) $castContext = $properties->all(); foreach ($properties as $name => $value) { - $dataProperty = $class->properties->first(fn(DataProperty $dataProperty) => $dataProperty->name === $name); + $dataProperty = $class->properties->first(fn (DataProperty $dataProperty) => $dataProperty->name === $name); if ($dataProperty === null) { continue; @@ -63,7 +60,7 @@ protected function cast( return $property->type->dataClass::from($value); } - if($property->type->kind->isDataCollectable()){ + if ($property->type->kind->isDataCollectable()) { return $property->type->dataClass::collect($value, $property->type->dataCollectableClass); } diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index c4454b56..2879cd68 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -17,7 +17,7 @@ public function handle(mixed $payload, DataClass $class, Collection $properties) $class ->properties - ->filter(fn(DataProperty $property) => ! $properties->has($property->name)) + ->filter(fn (DataProperty $property) => ! $properties->has($property->name)) ->each(function (DataProperty $property) use ($dataDefaults, &$properties) { if (array_key_exists($property->name, $dataDefaults)) { $properties[$property->name] = $dataDefaults[$property->name]; diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 3be4fbc0..d2aefe27 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -6,25 +6,20 @@ use Exception; use Illuminate\Contracts\Pagination\CursorPaginator; use Illuminate\Contracts\Pagination\Paginator; -use Illuminate\Http\Request; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; -use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\CustomCreationMethodType; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotCreateDataCollectable; -use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\Creation\CollectableMetaData; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Types\PartialType; -use function Pest\Laravel\instance; class DataCollectableFromSomethingResolver { @@ -122,10 +117,10 @@ protected function normalizeItems( mixed $items, string $dataClass, ): array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator { - if($items instanceof PaginatedDataCollection + if ($items instanceof PaginatedDataCollection || $items instanceof CursorPaginatedDataCollection || $items instanceof DataCollection - ){ + ) { $items = $items->items(); } @@ -194,6 +189,6 @@ protected function normalizeToCursorPaginator( protected function itemsToDataClosure(string $dataClass): Closure { - return fn(mixed $data) => $data instanceof $dataClass ? $data : $dataClass::from($data); + return fn (mixed $data) => $data instanceof $dataClass ? $data : $dataClass::from($data); } } diff --git a/src/Resolvers/PartialsTreeFromRequestResolver.php b/src/Resolvers/PartialsTreeFromRequestResolver.php index f4ae0394..0fe1bb81 100644 --- a/src/Resolvers/PartialsTreeFromRequestResolver.php +++ b/src/Resolvers/PartialsTreeFromRequestResolver.php @@ -5,11 +5,9 @@ use Illuminate\Http\Request; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Support\AllowedPartialsParser; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\PartialsParser; -use Spatie\LaravelData\Support\PartialTrees; use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use TypeError; diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index f724b30d..e90056b1 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -4,38 +4,21 @@ use Closure; use Exception; -use Faker\Provider\Base; use Illuminate\Contracts\Pagination\Paginator; use Illuminate\Pagination\CursorPaginator; use Illuminate\Support\Arr; use Illuminate\Support\Enumerable; -use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; -use Spatie\LaravelData\Lazy; -use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Lazy\ConditionalLazy; -use Spatie\LaravelData\Support\Lazy\RelationalLazy; -use Spatie\LaravelData\Support\PartialTrees; use Spatie\LaravelData\Support\Transformation\TransformationContext; -use Spatie\LaravelData\Support\TreeNodes\AllTreeNode; -use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; -use Spatie\LaravelData\Support\TreeNodes\PartialTreeNode; use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Support\Wrapping\WrapType; -use Spatie\LaravelData\Transformers\ArrayableTransformer; -use Spatie\LaravelData\Transformers\Transformer; -use TypeError; -use function Pest\Laravel\instance; class TransformedDataCollectionResolver { @@ -98,7 +81,7 @@ protected function transformPaginator( TransformationContext $context, TransformationContext $nestedContext, ): array { - $paginator->through(fn(BaseData $data) => $this->transformationClosure($nestedContext)($data)); + $paginator->through(fn (BaseData $data) => $this->transformationClosure($nestedContext)($data)); if ($context->transformValues === false) { return $paginator->all(); diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index f332b723..3482053a 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -6,7 +6,6 @@ use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Enums\DataTypeKind; @@ -16,7 +15,6 @@ use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; -use Spatie\LaravelData\Support\PartialTrees; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\TreeNodes\AllTreeNode; use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; @@ -24,8 +22,6 @@ use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Transformers\ArrayableTransformer; use Spatie\LaravelData\Transformers\Transformer; -use TypeError; -use function Pest\Laravel\instance; class TransformedDataResolver { diff --git a/src/Support/Annotations/DataCollectableAnnotation.php b/src/Support/Annotations/DataCollectableAnnotation.php index 4b89548e..905463c0 100644 --- a/src/Support/Annotations/DataCollectableAnnotation.php +++ b/src/Support/Annotations/DataCollectableAnnotation.php @@ -8,7 +8,6 @@ public function __construct( public string $dataClass, public ?string $collectionClass = null, public ?string $property = null, - ) - { + ) { } } diff --git a/src/Support/Annotations/DataCollectableAnnotationReader.php b/src/Support/Annotations/DataCollectableAnnotationReader.php index 7f804398..3f76d2cc 100644 --- a/src/Support/Annotations/DataCollectableAnnotationReader.php +++ b/src/Support/Annotations/DataCollectableAnnotationReader.php @@ -6,12 +6,10 @@ use Illuminate\Pagination\AbstractPaginator; use Illuminate\Support\Arr; use Illuminate\Support\Enumerable; -use Illuminate\Support\Str; use phpDocumentor\Reflection\FqsenResolver; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\ContextFactory; use ReflectionClass; -use ReflectionParameter; use ReflectionProperty; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; @@ -29,7 +27,7 @@ public static function create(): self /** @return array */ public function getForClass(ReflectionClass $class): array { - return collect($this->get($class))->keyBy(fn(DataCollectableAnnotation $annotation) => $annotation->property)->all(); + return collect($this->get($class))->keyBy(fn (DataCollectableAnnotation $annotation) => $annotation->property)->all(); } public function getForProperty(ReflectionProperty $property): ?DataCollectableAnnotation diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 90830f6e..08ba5bdd 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -50,12 +50,12 @@ public function __construct( public static function create(ReflectionClass $class): self { $attributes = collect($class->getAttributes()) - ->filter(fn(ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) - ->map(fn(ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); $methods = collect($class->getMethods()); - $constructor = $methods->first(fn(ReflectionMethod $method) => $method->isConstructor()); + $constructor = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor()); $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); @@ -88,10 +88,10 @@ protected static function resolveMethods( ReflectionClass $reflectionClass, ): Collection { return collect($reflectionClass->getMethods()) - ->filter(fn(ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collect')) - ->reject(fn(ReflectionMethod $method) => in_array($method->name, ['from', 'collect', 'collection'])) + ->filter(fn (ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collect')) + ->reject(fn (ReflectionMethod $method) => in_array($method->name, ['from', 'collect', 'collection'])) ->mapWithKeys( - fn(ReflectionMethod $method) => [$method->name => DataMethod::create($method)], + fn (ReflectionMethod $method) => [$method->name => DataMethod::create($method)], ); } @@ -104,9 +104,9 @@ protected static function resolveProperties( $defaultValues = self::resolveDefaultValues($class, $constructorMethod); return collect($class->getProperties(ReflectionProperty::IS_PUBLIC)) - ->reject(fn(ReflectionProperty $property) => $property->isStatic()) + ->reject(fn (ReflectionProperty $property) => $property->isStatic()) ->values() - ->mapWithKeys(fn(ReflectionProperty $property) => [ + ->mapWithKeys(fn (ReflectionProperty $property) => [ $property->name => DataProperty::create( $property, array_key_exists($property->getName(), $defaultValues), @@ -127,8 +127,8 @@ protected static function resolveDefaultValues( } $values = collect($constructorMethod->getParameters()) - ->filter(fn(ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) - ->mapWithKeys(fn(ReflectionParameter $parameter) => [ + ->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) + ->mapWithKeys(fn (ReflectionParameter $parameter) => [ $parameter->name => $parameter->getDefaultValue(), ]) ->toArray(); diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 30d3b0b0..01c1c06c 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -3,11 +3,8 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; -use ReflectionIntersectionType; use ReflectionMethod; -use ReflectionNamedType; use ReflectionParameter; -use ReflectionUnionType; use Spatie\LaravelData\Enums\CustomCreationMethodType; use Spatie\LaravelData\Support\Types\Type; use Spatie\LaravelData\Support\Types\UndefinedType; @@ -37,7 +34,7 @@ public static function create(ReflectionMethod $method): self return new self( $method->name, collect($method->getParameters())->map( - fn(ReflectionParameter $parameter) => DataParameter::create($parameter, $method->class), + fn (ReflectionParameter $parameter) => DataParameter::create($parameter, $method->class), ), $method->isStatic(), $method->isPublic(), @@ -99,7 +96,7 @@ public function accepts(mixed ...$input): bool /** @var Collection $parameters */ $parameters = array_is_list($input) ? $this->parameters - : $this->parameters->mapWithKeys(fn(DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); + : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); if (count($input) > $parameters->count()) { return false; @@ -116,17 +113,17 @@ public function accepts(mixed ...$input): bool continue; } - if( + if ( $parameter instanceof DataProperty && ! $parameter->type->type->acceptsValue($input[$index]) - ){ + ) { return false; } - if( + if ( $parameter instanceof DataParameter && ! $parameter->type->acceptsValue($input[$index]) - ){ + ) { return false; } } diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 60f5871a..d1ab7b4e 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -12,7 +12,6 @@ use Spatie\LaravelData\Mappers\NameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\Factories\DataTypeFactory; use Spatie\LaravelData\Transformers\Transformer; diff --git a/src/Support/DataType.php b/src/Support/DataType.php index 1fab0a09..45ffb9da 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -2,15 +2,8 @@ namespace Spatie\LaravelData\Support; -use ReflectionIntersectionType; -use ReflectionNamedType; -use ReflectionParameter; -use ReflectionProperty; -use ReflectionType; -use ReflectionUnionType; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\Types\Type; -use TypeError; class DataType { diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index d92425cf..f78e6050 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -2,40 +2,25 @@ namespace Spatie\LaravelData\Support\Factories; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Contracts\Pagination\Paginator; -use Illuminate\Pagination\AbstractCursorPaginator; -use Illuminate\Pagination\AbstractPaginator; use Illuminate\Support\Arr; -use Illuminate\Support\Enumerable; use ReflectionIntersectionType; use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; use ReflectionUnionType; use Spatie\LaravelData\Attributes\DataCollectionOf; -use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\CursorPaginatedDataCollection; -use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotFindDataClass; use Spatie\LaravelData\Exceptions\InvalidDataType; -use Spatie\LaravelData\Lazy; -use Spatie\LaravelData\Optional; -use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataType; -use Spatie\LaravelData\Support\Type; use Spatie\LaravelData\Support\Types\IntersectionType; -use Spatie\LaravelData\Support\Types\MultiType; use Spatie\LaravelData\Support\Types\PartialType; use Spatie\LaravelData\Support\Types\SingleType; use Spatie\LaravelData\Support\Types\UndefinedType; use Spatie\LaravelData\Support\Types\UnionType; use TypeError; -use function Pest\Laravel\instance; class DataTypeFactory { @@ -50,7 +35,7 @@ public function build( ): DataType { $type = $property->getType(); - $class = match ($property::class){ + $class = match ($property::class) { ReflectionParameter::class => $property->getDeclaringClass()?->name, ReflectionProperty::class => $property->class, }; diff --git a/src/Support/Partials/ForwardsToPartialsDefinition.php b/src/Support/Partials/ForwardsToPartialsDefinition.php index ec9d68ae..7c14e26b 100644 --- a/src/Support/Partials/ForwardsToPartialsDefinition.php +++ b/src/Support/Partials/ForwardsToPartialsDefinition.php @@ -6,7 +6,7 @@ trait ForwardsToPartialsDefinition { - protected abstract function getPartialsDefinition(): PartialsDefinition; + abstract protected function getPartialsDefinition(): PartialsDefinition; public function include(string ...$includes): static { diff --git a/src/Support/Partials/PartialsDefinition.php b/src/Support/Partials/PartialsDefinition.php index bafafec1..6d9a4852 100644 --- a/src/Support/Partials/PartialsDefinition.php +++ b/src/Support/Partials/PartialsDefinition.php @@ -17,7 +17,6 @@ public function __construct( public array $excludes = [], public array $only = [], public array $except = [], - ) - { + ) { } } diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index 30bef46c..8d06fb3f 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Support\Transformation; -use Closure; use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Wrapping\Wrap; diff --git a/src/Support/Transformation/PartialTransformationContext.php b/src/Support/Transformation/PartialTransformationContext.php index dc166172..59bf978f 100644 --- a/src/Support/Transformation/PartialTransformationContext.php +++ b/src/Support/Transformation/PartialTransformationContext.php @@ -5,13 +5,10 @@ use Closure; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\PartialsParser; use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; use Spatie\LaravelData\Support\TreeNodes\TreeNode; -use Spatie\LaravelData\Support\Wrapping\Wrap; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; class PartialTransformationContext { @@ -27,7 +24,7 @@ public static function create( BaseData|BaseDataCollectable $data, PartialsDefinition $partialsDefinition, ): self { - $filter = fn(bool|null|Closure $condition, string $definition) => match (true) { + $filter = fn (bool|null|Closure $condition, string $definition) => match (true) { is_bool($condition) => $condition, $condition === null => false, is_callable($condition) => $condition($data), diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index be77e152..b7fa2db4 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -2,15 +2,7 @@ namespace Spatie\LaravelData\Support\Transformation; -use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\DataCollectable; -use Spatie\LaravelData\Contracts\TransformableData; -use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; -use Spatie\LaravelData\Support\TreeNodes\TreeNode; -use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use TypeError; class TransformationContext { diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 270b858c..76c8defa 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -2,17 +2,10 @@ namespace Spatie\LaravelData\Support\Transformation; -use Closure; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\TransformableData; -use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; use Spatie\LaravelData\Support\Partials\PartialsDefinition; -use Spatie\LaravelData\Support\PartialsParser; -use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; -use Spatie\LaravelData\Support\TreeNodes\TreeNode; -use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; class TransformationContextFactory diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index b3f95116..9625379e 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -14,7 +14,6 @@ use ReflectionProperty; use RuntimeException; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Enums\DataCollectableType; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; diff --git a/src/Support/Types/IntersectionType.php b/src/Support/Types/IntersectionType.php index f7cd4ff0..e60408d8 100644 --- a/src/Support/Types/IntersectionType.php +++ b/src/Support/Types/IntersectionType.php @@ -2,9 +2,6 @@ namespace Spatie\LaravelData\Support\Types; -use ReflectionIntersectionType; -use ReflectionUnionType; - class IntersectionType extends MultiType { public function acceptsType(string $type): bool diff --git a/src/Support/Types/MultiType.php b/src/Support/Types/MultiType.php index 5f579dba..2d2e8cf5 100644 --- a/src/Support/Types/MultiType.php +++ b/src/Support/Types/MultiType.php @@ -21,8 +21,7 @@ public function __construct( public static function create( ReflectionUnionType|ReflectionIntersectionType $multiType, ?string $class, - ): static - { + ): static { $isNullable = $multiType->allowsNull(); $isMixed = false; $types = []; @@ -61,7 +60,7 @@ public function acceptedTypesCount(): int { return count(array_filter( $this->types, - fn(PartialType $subType) => ! $subType->isLazy() && ! $subType->isOptional() + fn (PartialType $subType) => ! $subType->isLazy() && ! $subType->isOptional() )); } } diff --git a/src/Support/Types/Type.php b/src/Support/Types/Type.php index ee882d4b..9259b8da 100644 --- a/src/Support/Types/Type.php +++ b/src/Support/Types/Type.php @@ -2,8 +2,6 @@ namespace Spatie\LaravelData\Support\Types; -use Exception; -use ReflectionClass; use ReflectionIntersectionType; use ReflectionNamedType; use ReflectionType; @@ -20,8 +18,7 @@ public function __construct( public static function forReflection( ?ReflectionType $type, string $class, - ): self - { + ): self { return match (true) { $type instanceof ReflectionNamedType => SingleType::create($type, $class), $type instanceof ReflectionUnionType => UnionType::create($type, $class), @@ -35,7 +32,7 @@ abstract public function acceptsType(string $type): bool; abstract public function findAcceptedTypeForBaseType(string $class): ?string; // TODO: remove this? - abstract public function getAcceptedTypes(): array; + abstract public function getAcceptedTypes(): array; public function acceptsValue(mixed $value): bool { @@ -55,5 +52,4 @@ public function acceptsValue(mixed $value): bool return $this->acceptsType($type); } - } diff --git a/src/Support/Types/UnionType.php b/src/Support/Types/UnionType.php index 25c09e15..34e570f6 100644 --- a/src/Support/Types/UnionType.php +++ b/src/Support/Types/UnionType.php @@ -2,8 +2,6 @@ namespace Spatie\LaravelData\Support\Types; -use ReflectionUnionType; - class UnionType extends MultiType { public function acceptsType(string $type): bool diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 89968177..aa53d1ff 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -21,7 +21,7 @@ use function Spatie\Snapshots\assertMatchesSnapshot; it('can get a paginated data collection', function () { - $items = Collection::times(100, fn(int $index) => "Item {$index}"); + $items = Collection::times(100, fn (int $index) => "Item {$index}"); $paginator = new LengthAwarePaginator( $items->forPage(1, 15), @@ -36,7 +36,7 @@ }); it('can get a paginated cursor data collection', function () { - $items = Collection::times(100, fn(int $index) => "Item {$index}"); + $items = Collection::times(100, fn (int $index) => "Item {$index}"); $paginator = new CursorPaginator( $items, @@ -71,7 +71,7 @@ test('a collection can be filtered', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); - $filtered = $collection->filter(fn(SimpleData $data) => $data->string === 'A')->toArray(); + $filtered = $collection->filter(fn (SimpleData $data) => $data->string === 'A')->toArray(); expect([ ['string' => 'A'], @@ -82,7 +82,7 @@ test('a collection can be transformed', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); - $filtered = $collection->through(fn(SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); + $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); expect($filtered)->toMatchArray([ ['string' => 'Ax'], @@ -96,7 +96,7 @@ new LengthAwarePaginator(['A', 'B'], 2, 15), ); - $filtered = $collection->through(fn(SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); + $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); expect($filtered['data'])->toMatchArray([ ['string' => 'Ax'], @@ -343,7 +343,7 @@ $data->string = strtoupper($data->string); return $data; - })->filter(fn(SimpleData $data) => $data->string === strtoupper('Never gonna give you up!'))->toArray(); + })->filter(fn (SimpleData $data) => $data->string === strtoupper('Never gonna give you up!'))->toArray(); expect($transformed)->toMatchArray([ ['string' => strtoupper('Never gonna give you up!')], @@ -441,7 +441,7 @@ public function __construct(public string $string) }); it('can return a custom paginated data collection when collecting data', function () { - $class = new class ('') extends Data implements DeprecatedDataContract{ + $class = new class ('') extends Data implements DeprecatedDataContract { use DeprecatedData; protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; diff --git a/tests/DataPipes/CastPropertiesDataPipeTest.php b/tests/DataPipes/CastPropertiesDataPipeTest.php index 6f633a5e..c8ca41e2 100644 --- a/tests/DataPipes/CastPropertiesDataPipeTest.php +++ b/tests/DataPipes/CastPropertiesDataPipeTest.php @@ -186,12 +186,12 @@ it('works nicely with lazy data', function () { $data = NestedLazyData::from([ - 'simple' => Lazy::create(fn() => SimpleData::from('Hello')), + 'simple' => Lazy::create(fn () => SimpleData::from('Hello')), ]); expect($data->simple) ->toBeInstanceOf(Lazy::class) - ->toEqual(Lazy::create(fn() => SimpleData::from('Hello'))); + ->toEqual(Lazy::create(fn () => SimpleData::from('Hello'))); }); it('allows casting', function () { diff --git a/tests/DataPipes/DefaultValuesDataPipeTest.php b/tests/DataPipes/DefaultValuesDataPipeTest.php index 83b2bf06..2097a1be 100644 --- a/tests/DataPipes/DefaultValuesDataPipeTest.php +++ b/tests/DataPipes/DefaultValuesDataPipeTest.php @@ -34,4 +34,3 @@ public static function defaults(): array expect(new $dataClass('Hi'))->toEqual($dataClass::from([])); }); - diff --git a/tests/DataPipes/MapPropertiesDataPipeTest.php b/tests/DataPipes/MapPropertiesDataPipeTest.php index 48561d5b..200299f0 100644 --- a/tests/DataPipes/MapPropertiesDataPipeTest.php +++ b/tests/DataPipes/MapPropertiesDataPipeTest.php @@ -3,7 +3,6 @@ use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Data; -use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Tests\Fakes\DataWithMapper; use Spatie\LaravelData\Tests\Fakes\SimpleData; diff --git a/tests/DataTest.php b/tests/DataTest.php index fd1df735..47efc2ca 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -5,9 +5,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; use Illuminate\Foundation\Http\FormRequest; -use Illuminate\Support\Arr; use Illuminate\Support\Collection; -use Illuminate\Support\Enumerable; use Inertia\LazyProp; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapOutputName; @@ -17,7 +15,6 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; -use Spatie\LaravelData\Concerns\DataTrait; use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; @@ -32,10 +29,7 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Lazy\InertiaLazy; -use Spatie\LaravelData\Support\PartialTrees; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCast; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCollectionCast; use Spatie\LaravelData\Tests\Fakes\Casts\ContextAwareCast; @@ -44,7 +38,6 @@ use Spatie\LaravelData\Tests\Fakes\DataWithMapper; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; use Spatie\LaravelData\Tests\Fakes\DummyDto; -use Spatie\LaravelData\Tests\Fakes\EmptyData; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\ExceptData; @@ -97,7 +90,7 @@ }); it('can include a lazy property', function () { - $data = new LazyData(Lazy::create(fn() => 'test')); + $data = new LazyData(Lazy::create(fn () => 'test')); // expect($data->toArray())->toBe([]); @@ -133,8 +126,8 @@ public function __construct( $data = new \TestIncludeableNestedLazyDataProperties( - Lazy::create(fn() => LazyData::from('Hello')), - Lazy::create(fn() => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), + Lazy::create(fn () => LazyData::from('Hello')), + Lazy::create(fn () => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), ); expect((clone $data)->toArray())->toBe([]); @@ -184,7 +177,7 @@ public function __construct( } } - $collection = Lazy::create(fn() => MultiLazyData::collect([ + $collection = Lazy::create(fn () => MultiLazyData::collect([ DummyDto::rick(), DummyDto::bon(), ])); @@ -240,7 +233,7 @@ public function __construct( public static function create(string $name): static { return new self( - Lazy::when(fn() => $name === 'Ruben', fn() => $name) + Lazy::when(fn () => $name === 'Ruben', fn () => $name) ); } }; @@ -264,7 +257,7 @@ public function __construct( public static function create(string $name): static { return new self( - Lazy::when(fn() => $name === 'Ruben', fn() => $name) + Lazy::when(fn () => $name === 'Ruben', fn () => $name) ); } }; @@ -326,7 +319,7 @@ public function __construct( public static function create(string $name): static { return new self( - Lazy::inertia(fn() => $name) + Lazy::inertia(fn () => $name) ); } }; @@ -337,7 +330,7 @@ public static function create(string $name): static }); it('can get the empty version of a data object', function () { - $dataClass = new class extends Data { + $dataClass = new class () extends Data { public string $property; public string|Lazy $lazyProperty; @@ -578,7 +571,7 @@ public function __construct( $data = new class ( $dataObject = new SimpleData('Test'), $dataCollection = new DataCollection(SimpleData::class, ['A', 'B']), - Lazy::create(fn() => new SimpleData('Lazy')), + Lazy::create(fn () => new SimpleData('Lazy')), 'Test', $transformable = new DateTime('16 may 1994') ) extends Data { @@ -695,7 +688,7 @@ public function __construct(public string $name) $transformed = $data->additional([ 'company' => 'Spatie', - 'alt_name' => fn(Data $data) => "{$data->name} from Spatie", + 'alt_name' => fn (Data $data) => "{$data->name} from Spatie", ])->toArray(); expect($transformed)->toMatchArray([ @@ -977,7 +970,7 @@ public function __construct( public Data $nestedData, #[ WithTransformer(ConfidentialDataCollectionTransformer::class), - DataCollectionOf(SimpleData::class) + DataCollectionOf(SimpleData::class) ] public array $nestedDataCollection, ) { @@ -1167,7 +1160,7 @@ public function __construct( }); it('will not include lazy optional values when transforming', function () { - $data = new class ('Hello World', Lazy::create(fn() => Optional::make())) extends Data { + $data = new class ('Hello World', Lazy::create(fn () => Optional::make())) extends Data { public function __construct( public string $string, public string|Optional|Lazy $lazy_optional_string, @@ -1225,7 +1218,7 @@ public function __construct( public array $nested_collection, #[ MapOutputName('nested_other_collection'), - DataCollectionOf(SimpleDataWithMappedProperty::class) + DataCollectionOf(SimpleDataWithMappedProperty::class) ] public array $nested_renamed_collection, ) { @@ -1365,7 +1358,7 @@ public static function fromData(Data $data) expect( MultiLazyData::from(DummyDto::rick()) - ->includeWhen('name', fn(MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') + ->includeWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') ->toArray() ) ->toMatchArray([ @@ -1390,7 +1383,7 @@ public static function fromData(Data $data) it('can conditionally include using class defaults', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createLazy(enabled: false)) @@ -1404,7 +1397,7 @@ public static function fromData(Data $data) it('can conditionally include using class defaults nested', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createLazy(enabled: true)) @@ -1414,8 +1407,8 @@ public static function fromData(Data $data) it('can conditionally include using class defaults multiple', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createLazy(enabled: false)) @@ -1433,8 +1426,8 @@ public static function fromData(Data $data) it('can conditionally exclude', function () { $data = new MultiLazyData( - Lazy::create(fn() => 'Rick Astley')->defaultIncluded(), - Lazy::create(fn() => 'Never gonna give you up')->defaultIncluded(), + Lazy::create(fn () => 'Rick Astley')->defaultIncluded(), + Lazy::create(fn () => 'Never gonna give you up')->defaultIncluded(), 1989 ); @@ -1453,7 +1446,7 @@ public static function fromData(Data $data) expect( (clone $data) - ->exceptWhen('name', fn(MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') + ->exceptWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') ->toArray() ) ->toMatchArray([ @@ -1467,7 +1460,7 @@ public static function fromData(Data $data) public NestedLazyData $nested; }; - $data->nested = new NestedLazyData(Lazy::create(fn() => SimpleData::from('Hello World'))->defaultIncluded()); + $data->nested = new NestedLazyData(Lazy::create(fn () => SimpleData::from('Hello World'))->defaultIncluded()); expect($data->toArray())->toMatchArray([ 'nested' => ['simple' => ['string' => 'Hello World']], @@ -1479,7 +1472,7 @@ public static function fromData(Data $data) it('can conditionally exclude using class defaults', function () { PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) @@ -1500,7 +1493,7 @@ public static function fromData(Data $data) it('can conditionally exclude using class defaults nested', function () { PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) @@ -1521,8 +1514,8 @@ public static function fromData(Data $data) it('can conditionally exclude using multiple class defaults', function () { PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) @@ -1558,15 +1551,15 @@ public static function fromData(Data $data) expect( (clone $data) - ->onlyWhen('second', fn(MultiData $data) => $data->second === 'World') + ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') ->toArray() ) ->toMatchArray(['second' => 'World']); expect( (clone $data) - ->onlyWhen('first', fn(MultiData $data) => $data->first === 'Hello') - ->onlyWhen('second', fn(MultiData $data) => $data->second === 'World') + ->onlyWhen('first', fn (MultiData $data) => $data->first === 'Hello') + ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') ->toArray() ) ->toMatchArray([ @@ -1600,7 +1593,7 @@ public static function fromData(Data $data) it('can conditionally define only using class defaults', function () { PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1618,7 +1611,7 @@ public static function fromData(Data $data) it('can conditionally define only using class defaults nested', function () { PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1638,8 +1631,8 @@ public static function fromData(Data $data) it('can conditionally define only using multiple class defaults', function () { PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1674,7 +1667,7 @@ public static function fromData(Data $data) expect( (clone $data) - ->exceptWhen('second', fn(MultiData $data) => $data->second === 'World') + ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') ) ->toArray() ->toMatchArray([ @@ -1683,8 +1676,8 @@ public static function fromData(Data $data) expect( (clone $data) - ->exceptWhen('first', fn(MultiData $data) => $data->first === 'Hello') - ->exceptWhen('second', fn(MultiData $data) => $data->second === 'World') + ->exceptWhen('first', fn (MultiData $data) => $data->first === 'Hello') + ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') ->toArray() )->toBeEmpty(); }); @@ -1707,7 +1700,7 @@ public static function fromData(Data $data) it('can conditionally define except using class defaults', function () { PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1728,7 +1721,7 @@ public static function fromData(Data $data) it('can conditionally define except using class defaults nested', function () { PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -1750,8 +1743,8 @@ public static function fromData(Data $data) it('can conditionally define except using multiple class defaults', function () { PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'string' => fn(PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn(PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, ]); expect(PartialClassConditionalData::create(enabled: false)) @@ -2240,7 +2233,7 @@ public function __construct( // TODO: extend tests here it('can use an array to store data', function () { - $dataClass = new class( + $dataClass = new class ( [LazyData::from('A'), LazyData::from('B')], collect([LazyData::from('A'), LazyData::from('B')]), ) extends Data { @@ -2275,11 +2268,11 @@ class TestSomeCustomCollection extends Collection { public function nameAll(): string { - return $this->map(fn($data) => $data->string)->join(', '); + return $this->map(fn ($data) => $data->string)->join(', '); } } - $dataClass = new class() extends Data { + $dataClass = new class () extends Data { public string $string; public static function fromString(string $string): self diff --git a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php index 194cf962..bfbbcb23 100644 --- a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; use Spatie\LaravelData\Casts\Cast; -use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Tests\Fakes\SimpleData; diff --git a/tests/Fakes/CollectionAnnotationsData.php b/tests/Fakes/CollectionAnnotationsData.php index 1497f5e4..b1f1100b 100644 --- a/tests/Fakes/CollectionAnnotationsData.php +++ b/tests/Fakes/CollectionAnnotationsData.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes; -use Illuminate\Pagination\AbstractPaginator; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Spatie\LaravelData\Attributes\DataCollectionOf; @@ -50,13 +49,13 @@ class CollectionAnnotationsData public DataCollection $propertyK; - /** @var array<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + /** @var array<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ public array $propertyL; - /** @var LengthAwarePaginator<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + /** @var LengthAwarePaginator<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ public LengthAwarePaginator $propertyM; - /** @var \Illuminate\Support\Collection<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ + /** @var \Illuminate\Support\Collection<\Spatie\LaravelData\Tests\Fakes\SimpleData> */ public Collection $propertyN; public DataCollection $propertyO; diff --git a/tests/Fakes/DataWithMapper.php b/tests/Fakes/DataWithMapper.php index 73fcba60..f2eca616 100644 --- a/tests/Fakes/DataWithMapper.php +++ b/tests/Fakes/DataWithMapper.php @@ -5,7 +5,6 @@ use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Data; -use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Mappers\SnakeCaseMapper; #[MapName(SnakeCaseMapper::class)] diff --git a/tests/Fakes/FakeModelData.php b/tests/Fakes/FakeModelData.php index 9969e1e1..f97ca1f9 100644 --- a/tests/Fakes/FakeModelData.php +++ b/tests/Fakes/FakeModelData.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Tests\Fakes; use Carbon\CarbonImmutable; -use Illuminate\Database\Eloquent\Casts\Attribute; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; diff --git a/tests/Fakes/Models/FakeModel.php b/tests/Fakes/Models/FakeModel.php index 6849dc11..01386168 100644 --- a/tests/Fakes/Models/FakeModel.php +++ b/tests/Fakes/Models/FakeModel.php @@ -23,7 +23,7 @@ public function fakeNestedModels(): HasMany public function accessor(): Attribute { - return Attribute::get(fn() => "accessor_{$this->string}"); + return Attribute::get(fn () => "accessor_{$this->string}"); } public function getOldAccessorAttribute() diff --git a/tests/Fakes/MultiNestedData.php b/tests/Fakes/MultiNestedData.php index 0ae81831..5c5c7464 100644 --- a/tests/Fakes/MultiNestedData.php +++ b/tests/Fakes/MultiNestedData.php @@ -4,7 +4,6 @@ use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Data; -use Spatie\LaravelData\DataCollection; class MultiNestedData extends Data { diff --git a/tests/Normalizers/ModelNormalizerTest.php b/tests/Normalizers/ModelNormalizerTest.php index f2bf30b5..6bd08a85 100644 --- a/tests/Normalizers/ModelNormalizerTest.php +++ b/tests/Normalizers/ModelNormalizerTest.php @@ -1,9 +1,5 @@ $property */ - class TestDataTypeWithClassAnnotatedProperty{ + class TestDataTypeWithClassAnnotatedProperty + { public function __construct( public array $property, ) { diff --git a/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php b/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php index 1e14b327..ab660150 100644 --- a/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php +++ b/tests/Support/EloquentCasts/DataCollectionEloquentCastTest.php @@ -2,9 +2,10 @@ use Illuminate\Support\Facades\DB; -use Spatie\LaravelData\DataCollection; use function Pest\Laravel\assertDatabaseHas; +use Spatie\LaravelData\DataCollection; + use Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCasts; use Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCustomCollectionCasts; use Spatie\LaravelData\Tests\Fakes\SimpleData; diff --git a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php index b5bce5b1..a9ad9c54 100644 --- a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php +++ b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php @@ -12,7 +12,6 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\TypeScriptTransformer\DataTypeScriptTransformer; -use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\SimpleData; use function Spatie\Snapshots\assertMatchesSnapshot as baseAssertMatchesSnapshot; From a87d4060dff2a5dbfec812aaa6a69057cf6ff02b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 2 Mar 2023 11:33:02 +0100 Subject: [PATCH 012/124] wip --- CHANGELOG.md | 1 + src/Concerns/BaseData.php | 2 -- src/Concerns/ContextableData.php | 2 ++ src/Contracts/BaseData.php | 2 -- src/Contracts/ContextableData.php | 10 ++++++++++ src/Contracts/IncludeableData.php | 2 +- src/Contracts/TransformableData.php | 2 +- src/Contracts/WrappableData.php | 2 +- src/Dto.php | 1 - tests/Support/DataTypeTest.php | 2 ++ 10 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 src/Contracts/ContextableData.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc994e5b..866034fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to `laravel-data` will be documented in this file. - Add support for using Laravel Model attributes as data properties - Add support for class defined defaults - Allow creating data objects using `from` without parameters +- Add support for a Dto and Resource object ## 3.1.0 - 2023-02-10 diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 8b6e66f7..14d2f569 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -26,8 +26,6 @@ trait BaseData { - protected ?DataContext $_dataContext = null; - public static function optional(mixed ...$payloads): ?static { if (count($payloads) === 0) { diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php index e79ff710..ac6db33b 100644 --- a/src/Concerns/ContextableData.php +++ b/src/Concerns/ContextableData.php @@ -11,6 +11,8 @@ trait ContextableData { + protected ?DataContext $_dataContext = null; + public function getDataContext(): DataContext { if ($this->_dataContext === null) { diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 6e1c3e29..6fdaa5b1 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -33,6 +33,4 @@ public static function normalizers(): array; public static function prepareForPipeline(\Illuminate\Support\Collection $properties): \Illuminate\Support\Collection; public static function pipeline(): DataPipeline; - - public function getDataContext(): DataContext; } diff --git a/src/Contracts/ContextableData.php b/src/Contracts/ContextableData.php new file mode 100644 index 00000000..8edbdf6d --- /dev/null +++ b/src/Contracts/ContextableData.php @@ -0,0 +1,10 @@ + Date: Fri, 7 Apr 2023 10:54:15 +0200 Subject: [PATCH 013/124] Add returns method --- src/Support/DataMethod.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 1fdcce89..c788ba26 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -137,4 +137,9 @@ public function accepts(mixed ...$input): bool return true; } + + public function returns(string $type): bool + { + return $this->returnType->acceptsType($type); + } } From 4b824816264c7d879d4025ac7706a2e832719d07 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 7 Apr 2023 08:55:51 +0000 Subject: [PATCH 014/124] Fix styling --- src/Concerns/BaseData.php | 1 - src/Concerns/ResponsableData.php | 6 +++--- src/Contracts/BaseData.php | 1 - src/Dto.php | 1 - src/Resolvers/DataFromSomethingResolver.php | 1 - tests/DataTest.php | 7 ++----- tests/Resolvers/EmptyDataResolverTest.php | 14 +++++++------- 7 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 14d2f569..4ccf6e7a 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -22,7 +22,6 @@ use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Transformation\DataContext; trait BaseData { diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 05d16de5..e665a529 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -25,9 +25,9 @@ public function toResponse($request) ->get($this) ->mergePartials( PartialTransformationContext::create( - $this, - $this->getDataContext()->partialsDefinition - ) + $this, + $this->getDataContext()->partialsDefinition + ) ); $context = $this instanceof IncludeableDataContract diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 6fdaa5b1..2f51def4 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -11,7 +11,6 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Transformation\DataContext; /** * @template TValue diff --git a/src/Dto.php b/src/Dto.php index 0371ae2b..396f30fa 100644 --- a/src/Dto.php +++ b/src/Dto.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData; use Spatie\LaravelData\Concerns\BaseData; -use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index a9dd0d84..321ac5ff 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -6,7 +6,6 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\CustomCreationMethodType; -use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; diff --git a/tests/DataTest.php b/tests/DataTest.php index f780bcc0..13d879e7 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -30,11 +30,8 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Lazy\InertiaLazy; -use Spatie\LaravelData\Support\PartialTrees; -use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use Spatie\LaravelData\Tests\Fakes\Castables\SimpleCastable; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Tests\Fakes\Castables\SimpleCastable; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCast; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCollectionCast; use Spatie\LaravelData\Tests\Fakes\Casts\ContextAwareCast; @@ -97,7 +94,7 @@ it('can include a lazy property', function () { $data = new LazyData(Lazy::create(fn () => 'test')); -// expect($data->toArray())->toBe([]); + // expect($data->toArray())->toBe([]); expect($data->include('name')->toArray()) ->toMatchArray([ diff --git a/tests/Resolvers/EmptyDataResolverTest.php b/tests/Resolvers/EmptyDataResolverTest.php index 3d725b31..e6d8ac6b 100644 --- a/tests/Resolvers/EmptyDataResolverTest.php +++ b/tests/Resolvers/EmptyDataResolverTest.php @@ -97,13 +97,13 @@ function assertEmptyPropertyValue( public Lazy|string|null $property; }); -// assertEmptyPropertyValue([], new class () { -// public Lazy|array|null $property; -// }); -// -// assertEmptyPropertyValue(['string' => null], new class () { -// public Lazy|SimpleData|null $property; -// }); + // assertEmptyPropertyValue([], new class () { + // public Lazy|array|null $property; + // }); + // + // assertEmptyPropertyValue(['string' => null], new class () { + // public Lazy|SimpleData|null $property; + // }); }); it('will return the base type for lazy types that can be optional', function () { From c494c1a96bfb67fc1881e040152999456c823b7e Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 7 Apr 2023 11:04:36 +0200 Subject: [PATCH 015/124] Fix benchmarks --- benchmarks/DataBench.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/benchmarks/DataBench.php b/benchmarks/DataBench.php index 51b46a41..c36e699c 100644 --- a/benchmarks/DataBench.php +++ b/benchmarks/DataBench.php @@ -49,11 +49,11 @@ public function benchDataTransformation() { $data = new MultiNestedData( new NestedData(new SimpleData('Hello')), - new DataCollection(NestedData::class, [ + [ new NestedData(new SimpleData('I')), new NestedData(new SimpleData('am')), new NestedData(new SimpleData('groot')), - ]) + ] ); $data->toArray(); @@ -85,10 +85,17 @@ public function benchDataCollectionCreation() ['string' => 'you'], ['string' => 'up'], ], + 'nestedArray' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], ] )->all(); - ComplicatedData::collection($collection); + ComplicatedData::collect($collection, DataCollection::class); } #[Revs(500), Iterations(2)] @@ -113,11 +120,16 @@ public function benchDataCollectionTransformation() new NestedData(new SimpleData('I')), new NestedData(new SimpleData('am')), new NestedData(new SimpleData('groot')), - ]) + ]), + [ + new NestedData(new SimpleData('I')), + new NestedData(new SimpleData('am')), + new NestedData(new SimpleData('groot')), + ], ) )->all(); - $collection = ComplicatedData::collection($collection); + $collection = ComplicatedData::collect($collection, DataCollection::class); $collection->toArray(); } From 71a31c2947f10aebe5e06e8029e7bdadd5507d99 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 7 Apr 2023 13:39:35 +0000 Subject: [PATCH 016/124] Fix styling --- src/Resolvers/PartialsTreeFromRequestResolver.php | 1 - src/Support/DataClass.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Resolvers/PartialsTreeFromRequestResolver.php b/src/Resolvers/PartialsTreeFromRequestResolver.php index 3cf8ac9d..c5cc911d 100644 --- a/src/Resolvers/PartialsTreeFromRequestResolver.php +++ b/src/Resolvers/PartialsTreeFromRequestResolver.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Support\AllowedPartialsParser; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\PartialsParser; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index d35eb20f..621b8cc7 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -18,9 +18,9 @@ use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Mappers\ProvidedNameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\Lazy\CachedLazy; use Spatie\LaravelData\Support\NameMapping\DataClassNameMapping; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; /** * @property class-string $name From 6562acf4410d17f4c8112e0160ef597b4669fba7 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 5 May 2023 17:43:11 +0200 Subject: [PATCH 017/124] Merge --- src/Support/Factories/DataTypeFactory.php | 14 +++++++++----- tests/DataCollectionTest.php | 4 ++-- tests/Support/DataTypeTest.php | 18 +++++++++--------- .../DataTypeScriptTransformerTest.php | 6 ++++-- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index f78e6050..ca02e4d4 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -62,7 +62,7 @@ protected function buildForEmptyType(): DataType { return new DataType( new UndefinedType(), - false, + null, false, DataTypeKind::Default, null, @@ -104,7 +104,7 @@ protected function buildForNamedType( return new DataType( type: $type, - isLazy: false, + lazyType: null, isOptional: false, kind: $kind, dataClass: $dataClass, @@ -123,14 +123,18 @@ protected function buildForMultiType( ReflectionIntersectionType::class => IntersectionType::create($multiReflectionType, $class), }; - $isLazy = false; $isOptional = false; $kind = DataTypeKind::Default; $dataClass = null; $dataCollectableClass = null; + $lazyType = null; + foreach ($type->types as $subType) { - $isLazy = $isLazy || $subType->isLazy(); + if($subType->isLazy()){ + $lazyType = $subType->name; + } + $isOptional = $isOptional || $subType->isOptional(); if (($subType->builtIn === false || $subType->name === 'array') @@ -163,7 +167,7 @@ protected function buildForMultiType( return new DataType( type: $type, - isLazy: $isLazy, + lazyType: $lazyType, isOptional: $isOptional, kind: $kind, dataClass: $dataClass, diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 652c9567..4a1c04a4 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -487,8 +487,8 @@ function (string $operation, array $arguments, array $expected) { }); test('a collection can be merged', function () { - $collectionA = SimpleData::collection(['A', 'B']); - $collectionB = SimpleData::collection(['C', 'D']); + $collectionA = SimpleData::collect(collect(['A', 'B'])); + $collectionB = SimpleData::collect(collect(['C', 'D'])); $filtered = $collectionA->merge($collectionB)->toArray(); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index cbe7c40c..a519b18c 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -303,7 +303,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeFalse() + ->lazyType->toBeNull() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) @@ -322,7 +322,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeTrue() + ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) @@ -360,7 +360,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeTrue() + ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) @@ -379,7 +379,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeFalse() + ->lazyType->toBeNull() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) @@ -417,7 +417,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeFalse() + ->lazyType->toBeNull() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) @@ -436,7 +436,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeTrue() + ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) @@ -474,7 +474,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeTrue() + ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) @@ -493,7 +493,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeFalse() + ->lazyType->toBeNull() ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) @@ -512,7 +512,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->isLazy->toBeTrue() + ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) diff --git a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php index 541f1004..df3f5bb8 100644 --- a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php +++ b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php @@ -29,7 +29,7 @@ function assertMatchesSnapshot($actual, Driver $driver = null): void it('can convert a data object to Typescript', function () { $config = TypeScriptTransformerConfig::create(); - $data = new class (null, Optional::create(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), SimpleData::from('Simple data'), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, [])) extends Data { + $data = new class (null, Optional::create(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), Lazy::closure(fn () => 'Lazy'), SimpleData::from('Simple data'), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, [])) extends Data { public function __construct( public null|int $nullable, public Optional|int $undefineable, @@ -217,7 +217,9 @@ public function __construct( public string $name, ) { } - }; + } + + ; $transformer = new DataTypeScriptTransformer($config); $reflection = new ReflectionClass(DummyTypeScriptOptionalClass::class); From f2f8df769c315c6fa4266a2fa521543745b8b9f0 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 5 May 2023 15:43:47 +0000 Subject: [PATCH 018/124] Fix styling --- src/Support/DataProperty.php | 3 ++- src/Support/Factories/DataTypeFactory.php | 2 +- tests/Support/DataTypeTest.php | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index c6f9e8dd..f50288ab 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -76,7 +76,8 @@ className: $property->class, validate: ! $attributes->contains( fn (object $attribute) => $attribute instanceof WithoutValidation ) && ! $computed, - computed: $computed, isPromoted: $property->isPromoted(), + computed: $computed, + isPromoted: $property->isPromoted(), isReadonly: $property->isReadOnly(), hasDefaultValue: $property->isPromoted() ? $hasDefaultValue : $property->hasDefaultValue(), defaultValue: $property->isPromoted() ? $defaultValue : $property->getDefaultValue(), diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index ca02e4d4..680ef05a 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -131,7 +131,7 @@ protected function buildForMultiType( foreach ($type->types as $subType) { - if($subType->isLazy()){ + if($subType->isLazy()) { $lazyType = $subType->name; } diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index a519b18c..17b2feb2 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -35,7 +35,6 @@ use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; -use Spatie\LaravelData\Tests\Fakes\CollectionAnnotationsData; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\SimpleData; From 6a85dbb2e2101ca22d2887d3327bd62681f4fa6f Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 21 Jun 2023 16:22:40 +0200 Subject: [PATCH 019/124] Add support for constructor parameter types --- .../DataCollectableAnnotationReader.php | 48 +++++++++++++------ src/Support/DataClass.php | 7 +++ tests/Fakes/CollectionAnnotationsData.php | 22 +++++++++ .../DataCollectableAnnotationReaderTest.php | 14 ++++++ tests/Support/DataTypeTest.php | 30 +++++++++--- 5 files changed, 100 insertions(+), 21 deletions(-) diff --git a/src/Support/Annotations/DataCollectableAnnotationReader.php b/src/Support/Annotations/DataCollectableAnnotationReader.php index 3f76d2cc..331f3158 100644 --- a/src/Support/Annotations/DataCollectableAnnotationReader.php +++ b/src/Support/Annotations/DataCollectableAnnotationReader.php @@ -10,6 +10,7 @@ use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\ContextFactory; use ReflectionClass; +use ReflectionMethod; use ReflectionProperty; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; @@ -24,7 +25,7 @@ public static function create(): self return new self(); } - /** @return array */ + /** @return array */ public function getForClass(ReflectionClass $class): array { return collect($this->get($class))->keyBy(fn (DataCollectableAnnotation $annotation) => $annotation->property)->all(); @@ -35,9 +36,15 @@ public function getForProperty(ReflectionProperty $property): ?DataCollectableAn return Arr::first($this->get($property)); } - /** @return \Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation[] */ + /** @return array */ + public function getForMethod(ReflectionMethod $method): array + { + return collect($this->get($method))->keyBy(fn (DataCollectableAnnotation $annotation) => $annotation->property)->all(); + } + + /** @return DataCollectableAnnotation[] */ protected function get( - ReflectionProperty|ReflectionClass $reflection + ReflectionProperty|ReflectionClass|ReflectionMethod $reflection ): array { $comment = $reflection->getDocComment(); @@ -47,13 +54,14 @@ protected function get( $comment = str_replace('?', '', $comment); - $kindPattern = '(?:@property|@var)\s*'; + $kindPattern = '(?:@property|@var|@param)\s*'; $fqsenPattern = '[\\\\a-z0-9_\|]+'; + $typesPattern = '[\\\\a-z0-9_\\|\\[\\]]+'; $keyPattern = '(?:int|string|\(int\|string\)|array-key)'; $parameterPattern = '\s*\$?(?[a-z0-9_]+)?'; preg_match_all( - "/{$kindPattern}(?{$fqsenPattern})\[\]{$parameterPattern}/i", + "/{$kindPattern}(?{$typesPattern}){$parameterPattern}/i", $comment, $arrayMatches, ); @@ -71,15 +79,27 @@ protected function get( } protected function resolveArrayAnnotations( - ReflectionProperty|ReflectionClass $reflection, + ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, array $arrayMatches ): array { $annotations = []; - foreach ($arrayMatches['dataClass'] as $index => $dataClass) { + foreach ($arrayMatches['types'] as $index => $types) { $parameter = $arrayMatches['parameter'][$index]; - $dataClass = $this->resolveDataClass($reflection, $dataClass); + $arrayType = Arr::first( + explode('|', $types), + fn(string $type) => str_contains($type, '[]'), + ); + + if(empty($arrayType)){ + continue; + } + + $dataClass = $this->resolveDataClass( + $reflection, + str_replace('[]', '', $arrayType) + ); if ($dataClass === null) { continue; @@ -96,7 +116,7 @@ protected function resolveArrayAnnotations( } protected function resolveCollectionAnnotations( - ReflectionProperty|ReflectionClass $reflection, + ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, array $collectionMatches ): array { $annotations = []; @@ -121,7 +141,7 @@ protected function resolveCollectionAnnotations( } protected function resolveDataClass( - ReflectionProperty|ReflectionClass $reflection, + ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, string $class ): ?string { if (str_contains($class, '|')) { @@ -150,7 +170,7 @@ protected function resolveDataClass( } protected function resolveCollectionClass( - ReflectionProperty|ReflectionClass $reflection, + ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, string $class ): ?string { if (str_contains($class, '|')) { @@ -190,7 +210,7 @@ protected function resolveCollectionClass( } protected function resolveFcqn( - ReflectionProperty|ReflectionClass $reflection, + ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, string $class ): ?string { $context = $this->getContext($reflection); @@ -201,9 +221,9 @@ protected function resolveFcqn( } - protected function getContext(ReflectionProperty|ReflectionClass $reflection): Context + protected function getContext(ReflectionProperty|ReflectionClass|ReflectionMethod $reflection): Context { - $reflectionClass = $reflection instanceof ReflectionProperty + $reflectionClass = $reflection instanceof ReflectionProperty || $reflection instanceof ReflectionMethod ? $reflection->getDeclaringClass() : $reflection; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 621b8cc7..e2d36269 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -63,6 +63,13 @@ public static function create(ReflectionClass $class): self $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); + if($constructor){ + $dataCollectablePropertyAnnotations = array_merge( + $dataCollectablePropertyAnnotations, + DataCollectableAnnotationReader::create()->getForMethod($constructor) + ); + } + $properties = self::resolveProperties( $class, $constructor, diff --git a/tests/Fakes/CollectionAnnotationsData.php b/tests/Fakes/CollectionAnnotationsData.php index b1f1100b..20180ad7 100644 --- a/tests/Fakes/CollectionAnnotationsData.php +++ b/tests/Fakes/CollectionAnnotationsData.php @@ -67,5 +67,27 @@ class CollectionAnnotationsData public array $propertyR; public array $propertyS; + public array $propertyT; + + /** + * @param \Spatie\LaravelData\Tests\Fakes\SimpleData[]|null $propertyA + * @param null|\Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyB + * @param ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyC + * @param ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyD + * @param \Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $propertyE + * @param ?\Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $propertyF + * @param SimpleData[] $propertyG + */ + public function method( + array $propertyA, + ?array $propertyB, + ?array $propertyC, + array $propertyD, + DataCollection $propertyE, + ?DataCollection $propertyF, + array $propertyG, + ) { + + } } diff --git a/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php b/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php index 1b54b328..cdff60e5 100644 --- a/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php +++ b/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php @@ -96,3 +96,17 @@ function (string $property, ?DataCollectableAnnotation $expected) { new DataCollectableAnnotation(SimpleData::class, property: 'propertyT'), ]); }); + +it('can get data class for a data collection by method annotation', function () { + $annotations = app(DataCollectableAnnotationReader::class)->getForMethod(new ReflectionMethod(CollectionAnnotationsData::class, 'method')); + + expect($annotations)->toEqualCanonicalizing([ + new DataCollectableAnnotation(SimpleData::class, property: 'propertyA'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyB'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyC'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyD'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyE'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyF'), + new DataCollectableAnnotation(SimpleData::class, property: 'propertyG'), + ]); +}); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index 17b2feb2..e915ed77 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -28,9 +28,8 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; +use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataType; -use Spatie\LaravelData\Support\Factories\DataTypeFactory; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; @@ -42,12 +41,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType { - $reflectionProperty = new ReflectionProperty($class, $property); + $class = DataClass::create(new ReflectionClass($class)); - return DataTypeFactory::create()->build( - $reflectionProperty, - DataCollectableAnnotationReader::create()->getForClass($reflectionProperty->getDeclaringClass())[$property] ?? null, - ); + return $class->properties->get($property)->type; } it('can deduce a type without definition', function () { @@ -932,6 +928,26 @@ public function __construct( ->dataCollectableClass->toBe('array'); }); +it('can annotate data collections using constructor parameter annotations', function () { + class TestDataTypeWithClassAnnotatedConstructorParam + { + /** + * @param array $property + */ + public function __construct( + public array $property, + ) { + } + } + + $type = resolveDataType(new \TestDataTypeWithClassAnnotatedConstructorParam([])); + + expect($type) + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array'); +}); + it('can deduce the types of lazy', function () { $type = resolveDataType(new class () { public SimpleData|Lazy $property; From 4d1176fac8392d9c7b42db77bdbd17640e4d84f3 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 21 Jun 2023 14:23:39 +0000 Subject: [PATCH 020/124] Fix styling --- src/Support/Annotations/DataCollectableAnnotationReader.php | 4 ++-- src/Support/DataClass.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Support/Annotations/DataCollectableAnnotationReader.php b/src/Support/Annotations/DataCollectableAnnotationReader.php index 331f3158..bcc5312f 100644 --- a/src/Support/Annotations/DataCollectableAnnotationReader.php +++ b/src/Support/Annotations/DataCollectableAnnotationReader.php @@ -89,10 +89,10 @@ protected function resolveArrayAnnotations( $arrayType = Arr::first( explode('|', $types), - fn(string $type) => str_contains($type, '[]'), + fn (string $type) => str_contains($type, '[]'), ); - if(empty($arrayType)){ + if(empty($arrayType)) { continue; } diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index e2d36269..f4c2918a 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -63,7 +63,7 @@ public static function create(ReflectionClass $class): self $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); - if($constructor){ + if($constructor) { $dataCollectablePropertyAnnotations = array_merge( $dataCollectablePropertyAnnotations, DataCollectableAnnotationReader::create()->getForMethod($constructor) From 3e887dd1baa2712d72f8d006fb9f514f9d965ec1 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 1 Aug 2023 15:14:33 +0200 Subject: [PATCH 021/124] Use ts transformer 3 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 41854ccc..99401163 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "phpstan/extension-installer" : "^1.1", "phpunit/phpunit" : "^9.3", "spatie/invade" : "^1.0", - "spatie/laravel-typescript-transformer" : "^2.1.6", + "spatie/typescript-transformer" : "v3.x-dev#40e9b98", "spatie/pest-plugin-snapshots" : "^1.1", "spatie/phpunit-snapshot-assertions" : "^4.2", "spatie/test-time" : "^1.2" From 4eea39ebe02570b52fe4a42acd483571d577eb73 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 1 Aug 2023 15:18:16 +0200 Subject: [PATCH 022/124] Remove old ts transformer stuff --- .../DataTypeScriptCollector.php | 22 --- .../DataTypeScriptTransformer.php | 144 +----------------- .../RemoveLazyTypeProcessor.php | 47 ------ .../RemoveOptionalTypeProcessor.php | 47 ------ 4 files changed, 3 insertions(+), 257 deletions(-) delete mode 100644 src/Support/TypeScriptTransformer/DataTypeScriptCollector.php delete mode 100644 src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php delete mode 100644 src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptCollector.php b/src/Support/TypeScriptTransformer/DataTypeScriptCollector.php deleted file mode 100644 index 364e40d1..00000000 --- a/src/Support/TypeScriptTransformer/DataTypeScriptCollector.php +++ /dev/null @@ -1,22 +0,0 @@ -isSubclassOf(BaseData::class)) { - return null; - } - - $transformer = new DataTypeScriptTransformer($this->config); - - return $transformer->transform($class, $class->getShortName()); - } -} diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index a7da38d6..296f316f 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -20,151 +20,13 @@ use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer; use Spatie\TypeScriptTransformer\Attributes\Optional as TypeScriptOptional; +use Spatie\TypeScriptTransformer\Laravel\Transformers\DataClassTransformer; use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; use Spatie\TypeScriptTransformer\TypeProcessors\DtoCollectionTypeProcessor; use Spatie\TypeScriptTransformer\TypeProcessors\ReplaceDefaultsTypeProcessor; use Spatie\TypeScriptTransformer\Types\StructType; -class DataTypeScriptTransformer extends DtoTransformer +class DataTypeScriptTransformer extends DataClassTransformer { - public function canTransform(ReflectionClass $class): bool - { - return $class->isSubclassOf(BaseData::class); - } - - protected function typeProcessors(): array - { - return [ - new ReplaceDefaultsTypeProcessor( - $this->config->getDefaultTypeReplacements() - ), - new RemoveLazyTypeProcessor(), - new RemoveOptionalTypeProcessor(), - new DtoCollectionTypeProcessor(), - ]; - } - - - protected function transformProperties( - ReflectionClass $class, - MissingSymbolsCollection $missingSymbols - ): string { - $dataClass = app(DataConfig::class)->getDataClass($class->getName()); - - $isOptional = $dataClass->attributes->contains( - fn (object $attribute) => $attribute instanceof TypeScriptOptional - ); - - return array_reduce( - $this->resolveProperties($class), - function (string $carry, ReflectionProperty $property) use ($isOptional, $dataClass, $missingSymbols) { - /** @var \Spatie\LaravelData\Support\DataProperty $dataProperty */ - $dataProperty = $dataClass->properties[$property->getName()]; - - $type = $this->resolveTypeForProperty($property, $dataProperty, $missingSymbols); - - if ($type === null) { - return $carry; - } - - $isOptional = $isOptional - || $dataProperty->attributes->contains( - fn (object $attribute) => $attribute instanceof TypeScriptOptional - ) - || ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class) - || $dataProperty->type->isOptional; - - $transformed = $this->typeToTypeScript( - $type, - $missingSymbols, - $property->getDeclaringClass()->getName(), - ); - - $propertyName = $dataProperty->outputMappedName ?? $dataProperty->name; - - if (! preg_match('/^[$_a-zA-Z][$_a-zA-Z0-9]*$/', $propertyName)) { - $propertyName = "'{$propertyName}'"; - } - - return $isOptional - ? "{$carry}{$propertyName}?: {$transformed};" . PHP_EOL - : "{$carry}{$propertyName}: {$transformed};" . PHP_EOL; - }, - '' - ); - } - - protected function resolveTypeForProperty( - ReflectionProperty $property, - DataProperty $dataProperty, - MissingSymbolsCollection $missingSymbols, - ): ?Type { - if (! $dataProperty->type->kind->isDataCollectable()) { - return $this->reflectionToType( - $property, - $missingSymbols, - ...$this->typeProcessors() - ); - } - - $collectionType = match ($dataProperty->type->kind) { - DataTypeKind::Enumerable, DataTypeKind::Array, DataTypeKind::DataCollection => $this->defaultCollectionType($dataProperty->type->dataClass), - DataTypeKind::Paginator, DataTypeKind::DataPaginatedCollection => $this->paginatedCollectionType($dataProperty->type->dataClass), - DataTypeKind::CursorPaginator, DataTypeKind::DataCursorPaginatedCollection => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), - default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') - }; - - if ($dataProperty->type->isNullable()) { - return new Nullable($collectionType); - } - - return $collectionType; - } - - protected function defaultCollectionType(string $class): Type - { - return new Array_(new Object_(new Fqsen("\\{$class}"))); - } - - protected function paginatedCollectionType(string $class): Type - { - return new StructType([ - 'data' => $this->defaultCollectionType($class), - 'links' => new Array_(new StructType([ - 'url' => new Nullable(new String_()), - 'label' => new String_(), - 'active' => new Boolean(), - ])), - 'meta' => new StructType([ - 'current_page' => new Integer(), - 'first_page_url' => new String_(), - 'from' => new Nullable(new Integer()), - 'last_page' => new Integer(), - 'last_page_url' => new String_(), - 'next_page_url' => new Nullable(new String_()), - 'path' => new String_(), - 'per_page' => new Integer(), - 'prev_page_url' => new Nullable(new String_()), - 'to' => new Nullable(new Integer()), - 'total' => new Integer(), - - ]), - ]); - } - - protected function cursorPaginatedCollectionType(string $class): Type - { - return new StructType([ - 'data' => $this->defaultCollectionType($class), - 'links' => new Array_(), - 'meta' => new StructType([ - 'path' => new String_(), - 'per_page' => new Integer(), - 'next_cursor' => new Nullable(new String_()), - 'next_cursor_url' => new Nullable(new String_()), - 'prev_cursor' => new Nullable(new String_()), - 'prev_cursor_url' => new Nullable(new String_()), - ]), - ]); - } + // TODO implement this ourselves } diff --git a/src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php b/src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php deleted file mode 100644 index ce18e176..00000000 --- a/src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php +++ /dev/null @@ -1,47 +0,0 @@ -getIterator())) - ->reject(function (Type $type) { - if (! $type instanceof Object_) { - return false; - } - - return is_a((string)$type->getFqsen(), Lazy::class, true); - }); - - if ($types->isEmpty()) { - throw new Exception("Type {$reflection->getDeclaringClass()->name}:{$reflection->getName()} cannot be only Lazy"); - } - - if ($types->count() === 1) { - return $types->first(); - } - - return new Compound($types->all()); - } -} diff --git a/src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php b/src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php deleted file mode 100644 index 274281ae..00000000 --- a/src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php +++ /dev/null @@ -1,47 +0,0 @@ -getIterator())) - ->reject(function (Type $type) { - if (! $type instanceof Object_) { - return false; - } - - return is_a((string)$type->getFqsen(), Optional::class, true); - }); - - if ($types->isEmpty()) { - throw new Exception("Type {$reflection->getDeclaringClass()->name}:{$reflection->getName()} cannot be only Optional"); - } - - if ($types->count() === 1) { - return $types->first(); - } - - return new Compound($types->all()); - } -} From 53de7d01b30e42db7acf4af2edcb097fb6c015c6 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 1 Aug 2023 13:18:48 +0000 Subject: [PATCH 023/124] Fix styling --- .../DataTypeScriptTransformer.php | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 296f316f..40532a7b 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -2,31 +2,9 @@ namespace Spatie\LaravelData\Support\TypeScriptTransformer; -use phpDocumentor\Reflection\Fqsen; -use phpDocumentor\Reflection\Type; -use phpDocumentor\Reflection\Types\Array_; -use phpDocumentor\Reflection\Types\Boolean; -use phpDocumentor\Reflection\Types\Integer; -use phpDocumentor\Reflection\Types\Nullable; -use phpDocumentor\Reflection\Types\Object_; -use phpDocumentor\Reflection\Types\String_; -use ReflectionClass; -use ReflectionProperty; -use RuntimeException; -use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Enums\DataTypeKind; -use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Lazy\ClosureLazy; -use Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer; -use Spatie\TypeScriptTransformer\Attributes\Optional as TypeScriptOptional; use Spatie\TypeScriptTransformer\Laravel\Transformers\DataClassTransformer; -use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; -use Spatie\TypeScriptTransformer\TypeProcessors\DtoCollectionTypeProcessor; -use Spatie\TypeScriptTransformer\TypeProcessors\ReplaceDefaultsTypeProcessor; -use Spatie\TypeScriptTransformer\Types\StructType; class DataTypeScriptTransformer extends DataClassTransformer { - // TODO implement this ourselves + // TODO implement this ourselves } From 5f566531c3c216e99a20b58bcddecbbf8225dbc3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 4 Aug 2023 10:03:07 +0200 Subject: [PATCH 024/124] wip --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 99401163..7eb218c5 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "phpstan/extension-installer" : "^1.1", "phpunit/phpunit" : "^9.3", "spatie/invade" : "^1.0", - "spatie/typescript-transformer" : "v3.x-dev#40e9b98", + "spatie/typescript-transformer" : "v3.x-dev#b89615c", "spatie/pest-plugin-snapshots" : "^1.1", "spatie/phpunit-snapshot-assertions" : "^4.2", "spatie/test-time" : "^1.2" From 0ea20b3ae557226bc3be5086b02508aaa76386cb Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 4 Aug 2023 11:10:43 +0200 Subject: [PATCH 025/124] Fix collection partials --- src/Resolvers/TransformedDataCollectionResolver.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index e90056b1..2f1607b6 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -15,6 +15,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; @@ -109,7 +110,12 @@ protected function transformationClosure( return $data; } - return app(TransformedDataResolver::class)->execute($data, $context); + $localPartials = PartialTransformationContext::create( + $data, + $data->getDataContext()->partialsDefinition + ); + + return app(TransformedDataResolver::class)->execute($data, $context->mergePartials($localPartials)); }; } } From 79c57dd27c2f666fc82fc7ce1bed38a64ce88399 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 4 Aug 2023 12:42:09 +0200 Subject: [PATCH 026/124] Remove test --- tests/DataCollectionTest.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 7565e7c4..7414875c 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -530,9 +530,3 @@ function (string $operation, array $arguments, array $expected) { expect($invaded->_dataContext)->toBeNull(); }); -it('can create an empty collection passing null', function () { - $collection = SimpleData::collection(null); - - expect($collection)->toBeInstanceOf(DataCollection::class); - expect($collection->toJson())->toBe('[]'); -}); From 8dbd8a7daa9b1f140641901671dda1a464705ce9 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 4 Aug 2023 10:42:34 +0000 Subject: [PATCH 027/124] Fix styling --- tests/DataCollectionTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 7414875c..4a1c04a4 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -529,4 +529,3 @@ function (string $operation, array $arguments, array $expected) { expect($invaded->_dataContext)->toBeNull(); }); - From ad1ac82780efc6517777e481a895c87612d1fc29 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 15 Sep 2023 14:07:45 +0000 Subject: [PATCH 028/124] Fix styling --- src/DataCollection.php | 1 - .../TypeScriptTransformer/DataTypeScriptTransformer.php | 4 ---- tests/Datasets/DataCollection.php | 1 - .../DataTypeScriptTransformerTest.php | 8 ++++---- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/DataCollection.php b/src/DataCollection.php index ad8063c9..945bdb0b 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -18,7 +18,6 @@ use Spatie\LaravelData\Exceptions\CannotCastData; use Spatie\LaravelData\Exceptions\InvalidDataCollectionOperation; use Spatie\LaravelData\Support\EloquentCasts\DataCollectionEloquentCast; -use Spatie\TypeScriptTransformer\Attributes\LiteralTypeScriptType; /** * @template TKey of array-key diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 267329fc..21494cce 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -3,12 +3,8 @@ namespace Spatie\LaravelData\Support\TypeScriptTransformer; use ReflectionClass; -use ReflectionProperty; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Support\DataConfig; -use Spatie\TypeScriptTransformer\Laravel\ClassPropertyProcessors\RemoveDataLazyTypeClassPropertyProcessor; use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; -use Spatie\TypeScriptTransformer\TypeScript\TypeScriptNode; class DataTypeScriptTransformer extends ClassTransformer { diff --git a/tests/Datasets/DataCollection.php b/tests/Datasets/DataCollection.php index 9b29f19f..cfda0e26 100644 --- a/tests/Datasets/DataCollection.php +++ b/tests/Datasets/DataCollection.php @@ -1,6 +1,5 @@ toBeInstanceOf(Transformed::class); - $someClass = new class { - + $someClass = new class () { }; $transformed = $transformer->transform( From 4bccae7d7f8e95ddac3f0014d81ffca0a968249b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 21 Sep 2023 17:11:35 +0200 Subject: [PATCH 029/124] Move tests --- tests/DataCollectionTest.php | 141 ----- tests/DataTest.php | 928 ---------------------------- tests/PartialsTest.php | 1099 ++++++++++++++++++++++++++++++++++ 3 files changed, 1099 insertions(+), 1069 deletions(-) create mode 100644 tests/PartialsTest.php diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 97f7b21b..5cf142d2 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -167,147 +167,6 @@ expect($collection)->toHaveCount(4); }); -it('has array access and will replicate partialtrees', function () { - $collection = MultiData::collect([ - new MultiData('first', 'second'), - ], DataCollection::class)->only('second'); - - expect($collection[0]->toArray())->toEqual(['second' => 'second']); -}); - -it('can dynamically include data based upon the request', function () { - LazyData::$allowedIncludes = ['']; - - $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); - - expect($response)->getData(true) - ->toMatchArray([ - [], - [], - [], - ]); - - LazyData::$allowedIncludes = ['name']; - - $includedResponse = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($includedResponse)->getData(true) - ->toMatchArray([ - ['name' => 'Ruben'], - ['name' => 'Freek'], - ['name' => 'Brent'], - ]); -}); - -it('can disabled manually including data in the request', function () { - LazyData::$allowedIncludes = []; - - $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($response)->getData(true) - ->toMatchArray([ - [], - [], - [], - ]); - - LazyData::$allowedIncludes = ['name']; - - $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($response)->getData(true) - ->toMatchArray([ - ['name' => 'Ruben'], - ['name' => 'Freek'], - ['name' => 'Brent'], - ]); - - LazyData::$allowedIncludes = null; - - $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($response)->getData(true) - ->toMatchArray([ - ['name' => 'Ruben'], - ['name' => 'Freek'], - ['name' => 'Brent'], - ]); -}); - -it('can dynamically exclude data based upon the request', function () { - DefaultLazyData::$allowedExcludes = []; - - $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); - - expect($response)->getData(true) - ->toMatchArray([ - ['name' => 'Ruben'], - ['name' => 'Freek'], - ['name' => 'Brent'], - ]); - - DefaultLazyData::$allowedExcludes = ['name']; - - $excludedResponse = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($excludedResponse)->getData(true) - ->toMatchArray([ - [], - [], - [], - ]); -}); - -it('can disable manually excluding data in the request', function () { - DefaultLazyData::$allowedExcludes = []; - - $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($response)->getData(true) - ->toMatchArray([ - ['name' => 'Ruben'], - ['name' => 'Freek'], - ['name' => 'Brent'], - ]); - - DefaultLazyData::$allowedExcludes = ['name']; - - $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($response)->getData(true) - ->toMatchArray([ - [], - [], - [], - ]); - - DefaultLazyData::$allowedExcludes = null; - - $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($response)->getData(true) - ->toMatchArray([ - [], - [], - [], - ]); -}); it('can update data properties withing a collection', function () { $collection = new DataCollection(LazyData::class, [ diff --git a/tests/DataTest.php b/tests/DataTest.php index cd116d93..96fa41f2 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -98,266 +98,6 @@ ]); }); -it('can include a lazy property', function () { - $data = new LazyData(Lazy::create(fn () => 'test')); - - // expect($data->toArray())->toBe([]); - - expect($data->include('name')->toArray()) - ->toMatchArray([ - 'name' => 'test', - ]); -}); - -it('can have a prefilled in lazy property', function () { - $data = new LazyData('test'); - - expect($data->toArray())->toMatchArray([ - 'name' => 'test', - ]); - - expect($data->include('name')->toArray()) - ->toMatchArray([ - 'name' => 'test', - ]); -}); - -it('can include a nested lazy property', function () { - class TestIncludeableNestedLazyDataProperties extends Data - { - public function __construct( - public LazyData|Lazy $data, - #[DataCollectionOf(LazyData::class)] - public array|Lazy $collection, - ) { - } - } - - - $data = new \TestIncludeableNestedLazyDataProperties( - Lazy::create(fn () => LazyData::from('Hello')), - Lazy::create(fn () => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), - ); - - expect((clone $data)->toArray())->toBe([]); - - expect((clone $data)->include('data')->toArray()) - ->toMatchArray([ - 'data' => [], - ]); - - expect((clone $data)->include('data.name')->toArray()) - ->toMatchArray([ - 'data' => ['name' => 'Hello'], - ]); - - expect((clone $data)->include('collection')->toArray()) - ->toMatchArray([ - 'collection' => [ - [], - [], - [], - [], - [], - [], - ], - ]); - - expect((clone $data)->include('collection.name')->toArray()) - ->toMatchArray([ - 'collection' => [ - ['name' => 'is'], - ['name' => 'it'], - ['name' => 'me'], - ['name' => 'your'], - ['name' => 'looking'], - ['name' => 'for'], - ], - ]); -}); - -it('can include specific nested data', function () { - class TestSpecificDefinedIncludeableCollectedAndNestedLazyData extends Data - { - public function __construct( - #[DataCollectionOf(MultiLazyData::class)] - public array|Lazy $songs - ) { - } - } - - $collection = Lazy::create(fn () => MultiLazyData::collect([ - DummyDto::rick(), - DummyDto::bon(), - ])); - - $data = new \TestSpecificDefinedIncludeableCollectedAndNestedLazyData($collection); - - expect($data->include('songs.name')->toArray()) - ->toMatchArray([ - 'songs' => [ - ['name' => DummyDto::rick()->name], - ['name' => DummyDto::bon()->name], - ], - ]); - - expect($data->include('songs.{name,artist}')->toArray()) - ->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - ], - ], - ]); - - expect($data->include('songs.*')->toArray()) - ->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - 'year' => DummyDto::rick()->year, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - 'year' => DummyDto::bon()->year, - ], - ], - ]); -}); - -it('can have a conditional lazy data', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|Lazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::when(fn () => $name === 'Ruben', fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->toArray())->toBe([]); - - $data = $blueprint::create('Ruben'); - - expect($data->toArray())->toMatchArray(['name' => 'Ruben']); -}); - -it('cannot have conditional lazy data manually loaded', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|Lazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::when(fn () => $name === 'Ruben', fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->include('name')->toArray())->toBeEmpty(); -}); - -it('can include data based upon relations loaded', function () { - $model = FakeNestedModel::factory()->create(); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); - - expect($transformed)->not->toHaveKey('fake_model'); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); - - expect($transformed)->toHaveKey('fake_model') - ->and($transformed['fake_model'])->toBeInstanceOf(FakeModelData::class); -}); - -it('can include data based upon relations loaded when they are null', function () { - $model = FakeNestedModel::factory(['fake_model_id' => null])->create(); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); - - expect($transformed)->not->toHaveKey('fake_model'); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); - - expect($transformed)->toHaveKey('fake_model') - ->and($transformed['fake_model'])->toBeNull(); -}); - -it('can have default included lazy data', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string|Lazy $name) - { - } - }; - - expect($data->toArray())->toMatchArray(['name' => 'Freek']); -}); - -it('can exclude default lazy data', function () { - $data = DefaultLazyData::from('Freek'); - - expect($data->exclude('name')->toArray())->toBe([]); -}); - -it('always transforms lazy inertia data to inertia lazy props', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|InertiaLazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::inertia(fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->toArray()['name'])->toBeInstanceOf(LazyProp::class); -}); - -it('always transforms closure lazy into closures for inertia', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|ClosureLazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::closure(fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->toArray()['name'])->toBeInstanceOf(Closure::class); -}); - it('can get the empty version of a data object', function () { $dataClass = new class () extends Data { public string $property; @@ -442,160 +182,6 @@ public function __construct( expect($data->toArray())->toMatchArray(['date' => null]); }); -it('can dynamically include data based upon the request', function () { - LazyData::$allowedIncludes = []; - - $response = LazyData::from('Ruben')->toResponse(request()); - - expect($response)->getData(true)->toBe([]); - - LazyData::$allowedIncludes = ['name']; - - $includedResponse = LazyData::from('Ruben')->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($includedResponse)->getData(true) - ->toMatchArray(['name' => 'Ruben']); -}); - -it('can disabled including data dynamically from the request', function () { - LazyData::$allowedIncludes = []; - - $response = LazyData::from('Ruben')->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($response->getData(true))->toBe([]); - - LazyData::$allowedIncludes = ['name']; - - $response = LazyData::from('Ruben')->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($response->getData(true))->toBeArray(['name' => 'Ruben']); - - LazyData::$allowedIncludes = null; - - $response = LazyData::from('Ruben')->toResponse(request()->merge([ - 'include' => 'name', - ])); - - expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); -}); - -it('can dynamically exclude data based upon the request', function () { - DefaultLazyData::$allowedExcludes = []; - - $response = DefaultLazyData::from('Ruben')->toResponse(request()); - - expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); - - DefaultLazyData::$allowedExcludes = ['name']; - - $excludedResponse = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($excludedResponse->getData(true))->toBe([]); -}); - -it('can disabled excluding data dynamically from the request', function () { - DefaultLazyData::$allowedExcludes = []; - - $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); - - DefaultLazyData::$allowedExcludes = ['name']; - - $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($response->getData(true))->toBe([]); - - DefaultLazyData::$allowedExcludes = null; - - $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ - 'exclude' => 'name', - ])); - - expect($response->getData(true))->toBe([]); -}); - -it('can disabled only data dynamically from the request', function () { - OnlyData::$allowedOnly = []; - - $response = OnlyData::from([ - 'first_name' => 'Ruben', - 'last_name' => 'Van Assche', - ])->toResponse(request()->merge([ - 'only' => 'first_name', - ])); - - expect($response->getData(true))->toBe([ - 'first_name' => 'Ruben', - 'last_name' => 'Van Assche', - ]); - - OnlyData::$allowedOnly = ['first_name']; - - $response = OnlyData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ - 'only' => 'first_name', - ])); - - expect($response->getData(true))->toMatchArray([ - 'first_name' => 'Ruben', - ]); - - OnlyData::$allowedOnly = null; - - $response = OnlyData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ - 'only' => 'first_name', - ])); - - expect($response->getData(true))->toMatchArray([ - 'first_name' => 'Ruben', - ]); -}); - -it('can disabled except data dynamically from the request', function () { - ExceptData::$allowedExcept = []; - - $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ - 'except' => 'first_name', - ])); - - expect($response->getData(true))->toMatchArray([ - 'first_name' => 'Ruben', - 'last_name' => 'Van Assche', - ]); - - ExceptData::$allowedExcept = ['first_name']; - - $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ - 'except' => 'first_name', - ])); - - expect($response->getData(true))->toMatchArray([ - 'last_name' => 'Van Assche', - ]); - - ExceptData::$allowedExcept = null; - - $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ - 'except' => 'first_name', - ])); - - expect($response->getData(true))->toMatchArray([ - 'last_name' => 'Van Assche', - ]); -}); - it('can get the data object without transforming', function () { $data = new class ( $dataObject = new SimpleData('Test'), @@ -1247,44 +833,6 @@ public function __construct( ]); }); -it('will not include lazy optional values when transforming', function () { - $data = new class ('Hello World', Lazy::create(fn () => Optional::make())) extends Data { - public function __construct( - public string $string, - public string|Optional|Lazy $lazy_optional_string, - ) { - } - }; - - expect($data->toArray())->toMatchArray([ - 'string' => 'Hello World', - ]); -}); - -it('excludes optional values data', function () { - $dataClass = new class () extends Data { - public string|Optional $name; - }; - - $data = $dataClass::from([]); - - expect($data->toArray())->toBe([]); -}); - -it('includes value if not optional data', function () { - $dataClass = new class () extends Data { - public string|Optional $name; - }; - - $data = $dataClass::from([ - 'name' => 'Freek', - ]); - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - ]); -}); - it('can map transformed property names', function () { $data = new SimpleDataWithMappedProperty('hello'); $dataCollection = SimpleDataWithMappedProperty::collect([ @@ -1429,465 +977,6 @@ public static function fromData(Data $data) ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); }); - -it('can conditionally include', function () { - expect( - MultiLazyData::from(DummyDto::rick())->includeWhen('artist', false)->toArray() - )->toBeEmpty(); - - expect( - MultiLazyData::from(DummyDto::rick()) - ->includeWhen('artist', true) - ->toArray() - ) - ->toMatchArray([ - 'artist' => 'Rick Astley', - ]); - - expect( - MultiLazyData::from(DummyDto::rick()) - ->includeWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') - ->toArray() - ) - ->toMatchArray([ - 'name' => 'Never gonna give you up', - ]); -}); - -it('can conditionally include nested', function () { - $data = new class () extends Data { - public NestedLazyData $nested; - }; - - $data->nested = NestedLazyData::from('Hello World'); - - expect($data->toArray())->toMatchArray(['nested' => []]); - - expect($data->includeWhen('nested.simple', true)->toArray()) - ->toMatchArray([ - 'nested' => ['simple' => ['string' => 'Hello World']], - ]); -}); - -it('can conditionally include using class defaults', function () { - PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::createLazy(enabled: false)) - ->toArray() - ->toMatchArray(['enabled' => false]); - - expect(PartialClassConditionalData::createLazy(enabled: true)) - ->toArray() - ->toMatchArray(['enabled' => true, 'string' => 'Hello World']); -}); - -it('can conditionally include using class defaults nested', function () { - PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::createLazy(enabled: true)) - ->toArray() - ->toMatchArray(['enabled' => true, 'nested' => ['string' => 'Hello World']]); -}); - -it('can conditionally include using class defaults multiple', function () { - PartialClassConditionalData::setDefinitions(includeDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::createLazy(enabled: false)) - ->toArray() - ->toMatchArray(['enabled' => false]); - - expect(PartialClassConditionalData::createLazy(enabled: true)) - ->toArray() - ->toMatchArray([ - 'enabled' => true, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); -}); - -it('can conditionally exclude', function () { - $data = new MultiLazyData( - Lazy::create(fn () => 'Rick Astley')->defaultIncluded(), - Lazy::create(fn () => 'Never gonna give you up')->defaultIncluded(), - 1989 - ); - - expect((clone $data)->exceptWhen('artist', false)->toArray()) - ->toMatchArray([ - 'artist' => 'Rick Astley', - 'name' => 'Never gonna give you up', - 'year' => 1989, - ]); - - expect((clone $data)->exceptWhen('artist', true)->toArray()) - ->toMatchArray([ - 'name' => 'Never gonna give you up', - 'year' => 1989, - ]); - - expect( - (clone $data) - ->exceptWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') - ->toArray() - ) - ->toMatchArray([ - 'artist' => 'Rick Astley', - 'year' => 1989, - ]); -}); - -it('can conditionally exclude nested', function () { - $data = new class () extends Data { - public NestedLazyData $nested; - }; - - $data->nested = new NestedLazyData(Lazy::create(fn () => SimpleData::from('Hello World'))->defaultIncluded()); - - expect($data->toArray())->toMatchArray([ - 'nested' => ['simple' => ['string' => 'Hello World']], - ]); - - expect($data->exceptWhen('nested.simple', true)->toArray()) - ->toMatchArray(['nested' => []]); -}); - -it('can conditionally exclude using class defaults', function () { - PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::createDefaultIncluded(enabled: true)) - ->toArray() - ->toMatchArray([ - 'enabled' => true, - 'nested' => ['string' => 'Hello World'], - ]); -}); - -it('can conditionally exclude using class defaults nested', function () { - PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::createDefaultIncluded(enabled: true)) - ->toArray() - ->toMatchArray([ - 'enabled' => true, - 'string' => 'Hello World', - ]); -}); - -it('can conditionally exclude using multiple class defaults', function () { - PartialClassConditionalData::setDefinitions(excludeDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::createDefaultIncluded(enabled: true)) - ->toArray() - ->toMatchArray(['enabled' => true]); -}); - -it('can conditionally define only', function () { - $data = new MultiData('Hello', 'World'); - - expect( - (clone $data)->onlyWhen('first', true)->toArray() - ) - ->toMatchArray([ - 'first' => 'Hello', - ]); - - expect( - (clone $data)->onlyWhen('first', false)->toArray() - ) - ->toMatchArray([ - 'first' => 'Hello', - 'second' => 'World', - ]); - - expect( - (clone $data) - ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') - ->toArray() - ) - ->toMatchArray(['second' => 'World']); - - expect( - (clone $data) - ->onlyWhen('first', fn (MultiData $data) => $data->first === 'Hello') - ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') - ->toArray() - ) - ->toMatchArray([ - 'first' => 'Hello', - 'second' => 'World', - ]); -}); - -it('can conditionally define only nested', function () { - $data = new class () extends Data { - public MultiData $nested; - }; - - $data->nested = new MultiData('Hello', 'World'); - - expect( - (clone $data)->onlyWhen('nested.first', true)->toArray() - )->toMatchArray([ - 'nested' => ['first' => 'Hello'], - ]); - - expect( - (clone $data)->onlyWhen('nested.{first, second}', true)->toArray() - )->toMatchArray([ - 'nested' => [ - 'first' => 'Hello', - 'second' => 'World', - ], - ]); -}); - -it('can conditionally define only using class defaults', function () { - PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::create(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::create(enabled: true)) - ->toArray() - ->toMatchArray(['string' => 'Hello World']); -}); - -it('can conditionally define only using class defaults nested', function () { - PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::create(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::create(enabled: true)) - ->toArray() - ->toMatchArray([ - 'nested' => ['string' => 'Hello World'], - ]); -}); - -it('can conditionally define only using multiple class defaults', function () { - PartialClassConditionalData::setDefinitions(onlyDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::create(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::create(enabled: true)) - ->toArray() - ->toMatchArray([ - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); -}); - -it('can conditionally define except', function () { - $data = new MultiData('Hello', 'World'); - - expect((clone $data)->exceptWhen('first', true)) - ->toArray() - ->toMatchArray(['second' => 'World']); - - expect((clone $data)->exceptWhen('first', false)) - ->toArray() - ->toMatchArray([ - 'first' => 'Hello', - 'second' => 'World', - ]); - - expect( - (clone $data) - ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') - ) - ->toArray() - ->toMatchArray([ - 'first' => 'Hello', - ]); - - expect( - (clone $data) - ->exceptWhen('first', fn (MultiData $data) => $data->first === 'Hello') - ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') - ->toArray() - )->toBeEmpty(); -}); - -it('can conditionally define except nested', function () { - $data = new class () extends Data { - public MultiData $nested; - }; - - $data->nested = new MultiData('Hello', 'World'); - - expect((clone $data)->exceptWhen('nested.first', true)) - ->toArray() - ->toMatchArray(['nested' => ['second' => 'World']]); - - expect((clone $data)->exceptWhen('nested.{first, second}', true)) - ->toArray() - ->toMatchArray(['nested' => []]); -}); - -it('can conditionally define except using class defaults', function () { - PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::create(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::create(enabled: true)) - ->toArray() - ->toMatchArray([ - 'enabled' => true, - 'nested' => ['string' => 'Hello World'], - ]); -}); - -it('can conditionally define except using class defaults nested', function () { - PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::create(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::create(enabled: true)) - ->toArray() - ->toMatchArray([ - 'enabled' => true, - 'string' => 'Hello World', - 'nested' => [], - ]); -}); - -it('can conditionally define except using multiple class defaults', function () { - PartialClassConditionalData::setDefinitions(exceptDefinitions: [ - 'string' => fn (PartialClassConditionalData $data) => $data->enabled, - 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, - ]); - - expect(PartialClassConditionalData::create(enabled: false)) - ->toArray() - ->toMatchArray([ - 'enabled' => false, - 'string' => 'Hello World', - 'nested' => ['string' => 'Hello World'], - ]); - - expect(PartialClassConditionalData::create(enabled: true)) - ->toArray() - ->toMatchArray([ - 'enabled' => true, - 'nested' => [], - ]); -}); - -test('only has precedence over except', function () { - $data = new MultiData('Hello', 'World'); - - expect( - (clone $data)->onlyWhen('first', true) - ->exceptWhen('first', true) - ->toArray() - )->toMatchArray(['second' => 'World']); - - expect( - (clone $data)->exceptWhen('first', true)->onlyWhen('first', true)->toArray() - )->toMatchArray(['second' => 'World']); -}); - -it('can perform only and except on array properties', function () { - $data = new class ('Hello World', ['string' => 'Hello World', 'int' => 42]) extends Data { - public function __construct( - public string $string, - public array $array - ) { - } - }; - - expect((clone $data)->only('string', 'array.int')) - ->toArray() - ->toMatchArray([ - 'string' => 'Hello World', - 'array' => ['int' => 42], - ]); - - expect((clone $data)->except('string', 'array.int')) - ->toArray() - ->toMatchArray([ - 'array' => ['string' => 'Hello World'], - ]); -}); - it('can wrap data objects', function () { expect( SimpleData::from('Hello World') @@ -2387,23 +1476,6 @@ public static function collectArray(array $items): \TestSomeCustomCollection ]); }); -it('can fetch lazy union data', function () { - $data = UnionData::from(1); - - expect($data->id)->toBe(1); - expect($data->simple->string)->toBe('A'); - expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); - expect($data->fakeModel->string)->toBe('lazy'); -}); - -it('can fetch non-lazy union data', function () { - $data = UnionData::from('A'); - - expect($data->id)->toBe(1); - expect($data->simple->string)->toBe('A'); - expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); - expect($data->fakeModel->string)->toBe('non-lazy'); -}); it('can set a default value for data object', function () { $dataObject = new class ('', '') extends Data { diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php new file mode 100644 index 00000000..c49fd2b2 --- /dev/null +++ b/tests/PartialsTest.php @@ -0,0 +1,1099 @@ + 'test')); + +// expect($data->toArray())->toBe([]); + + expect($data->include('name')->toArray()) + ->toMatchArray([ + 'name' => 'test', + ]); +}); + +it('can have a prefilled in lazy property', function () { + $data = new LazyData('test'); + + expect($data->toArray())->toMatchArray([ + 'name' => 'test', + ]); + + expect($data->include('name')->toArray()) + ->toMatchArray([ + 'name' => 'test', + ]); +}); + +it('can include a nested lazy property', function () { + class TestIncludeableNestedLazyDataProperties extends Data + { + public function __construct( + public LazyData|Lazy $data, + #[DataCollectionOf(LazyData::class)] + public array|Lazy $collection, + ) { + } + } + + + $data = new \TestIncludeableNestedLazyDataProperties( + Lazy::create(fn () => LazyData::from('Hello')), + Lazy::create(fn () => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), + ); + + expect((clone $data)->toArray())->toBe([]); + + expect((clone $data)->include('data')->toArray()) + ->toMatchArray([ + 'data' => [], + ]); + + expect((clone $data)->include('data.name')->toArray()) + ->toMatchArray([ + 'data' => ['name' => 'Hello'], + ]); + + expect((clone $data)->include('collection')->toArray()) + ->toMatchArray([ + 'collection' => [ + [], + [], + [], + [], + [], + [], + ], + ]); + + expect((clone $data)->include('collection.name')->toArray()) + ->toMatchArray([ + 'collection' => [ + ['name' => 'is'], + ['name' => 'it'], + ['name' => 'me'], + ['name' => 'your'], + ['name' => 'looking'], + ['name' => 'for'], + ], + ]); +}); + +it('can include specific nested data', function () { + class TestSpecificDefinedIncludeableCollectedAndNestedLazyData extends Data + { + public function __construct( + #[DataCollectionOf(MultiLazyData::class)] + public array|Lazy $songs + ) { + } + } + + $collection = Lazy::create(fn () => MultiLazyData::collect([ + DummyDto::rick(), + DummyDto::bon(), + ])); + + $data = new \TestSpecificDefinedIncludeableCollectedAndNestedLazyData($collection); + + expect($data->include('songs.name')->toArray()) + ->toMatchArray([ + 'songs' => [ + ['name' => DummyDto::rick()->name], + ['name' => DummyDto::bon()->name], + ], + ]); + + expect($data->include('songs.{name,artist}')->toArray()) + ->toMatchArray([ + 'songs' => [ + [ + 'name' => DummyDto::rick()->name, + 'artist' => DummyDto::rick()->artist, + ], + [ + 'name' => DummyDto::bon()->name, + 'artist' => DummyDto::bon()->artist, + ], + ], + ]); + + expect($data->include('songs.*')->toArray()) + ->toMatchArray([ + 'songs' => [ + [ + 'name' => DummyDto::rick()->name, + 'artist' => DummyDto::rick()->artist, + 'year' => DummyDto::rick()->year, + ], + [ + 'name' => DummyDto::bon()->name, + 'artist' => DummyDto::bon()->artist, + 'year' => DummyDto::bon()->year, + ], + ], + ]); +}); + +it('can have a conditional lazy data', function () { + $blueprint = new class () extends Data { + public function __construct( + public string|Lazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::when(fn () => $name === 'Ruben', fn () => $name) + ); + } + }; + + $data = $blueprint::create('Freek'); + + expect($data->toArray())->toBe([]); + + $data = $blueprint::create('Ruben'); + + expect($data->toArray())->toMatchArray(['name' => 'Ruben']); +}); + +it('cannot have conditional lazy data manually loaded', function () { + $blueprint = new class () extends Data { + public function __construct( + public string|Lazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::when(fn () => $name === 'Ruben', fn () => $name) + ); + } + }; + + $data = $blueprint::create('Freek'); + + expect($data->include('name')->toArray())->toBeEmpty(); +}); + +it('can include data based upon relations loaded', function () { + $model = FakeNestedModel::factory()->create(); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); + + expect($transformed)->not->toHaveKey('fake_model'); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); + + expect($transformed)->toHaveKey('fake_model') + ->and($transformed['fake_model'])->toBeInstanceOf(FakeModelData::class); +}); + +it('can include data based upon relations loaded when they are null', function () { + $model = FakeNestedModel::factory(['fake_model_id' => null])->create(); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); + + expect($transformed)->not->toHaveKey('fake_model'); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); + + expect($transformed)->toHaveKey('fake_model') + ->and($transformed['fake_model'])->toBeNull(); +}); + +it('can have default included lazy data', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string|Lazy $name) + { + } + }; + + expect($data->toArray())->toMatchArray(['name' => 'Freek']); +}); + +it('can exclude default lazy data', function () { + $data = DefaultLazyData::from('Freek'); + + expect($data->exclude('name')->toArray())->toBe([]); +}); + +it('always transforms lazy inertia data to inertia lazy props', function () { + $blueprint = new class () extends Data { + public function __construct( + public string|InertiaLazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::inertia(fn () => $name) + ); + } + }; + + $data = $blueprint::create('Freek'); + + expect($data->toArray()['name'])->toBeInstanceOf(LazyProp::class); +}); + +it('always transforms closure lazy into closures for inertia', function () { + $blueprint = new class () extends Data { + public function __construct( + public string|ClosureLazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::closure(fn () => $name) + ); + } + }; + + $data = $blueprint::create('Freek'); + + expect($data->toArray()['name'])->toBeInstanceOf(Closure::class); +}); + + +it('can dynamically include data based upon the request', function () { + LazyData::$allowedIncludes = []; + + $response = LazyData::from('Ruben')->toResponse(request()); + + expect($response)->getData(true)->toBe([]); + + LazyData::$allowedIncludes = ['name']; + + $includedResponse = LazyData::from('Ruben')->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($includedResponse)->getData(true) + ->toMatchArray(['name' => 'Ruben']); +}); + +it('can disabled including data dynamically from the request', function () { + LazyData::$allowedIncludes = []; + + $response = LazyData::from('Ruben')->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($response->getData(true))->toBe([]); + + LazyData::$allowedIncludes = ['name']; + + $response = LazyData::from('Ruben')->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($response->getData(true))->toBeArray(['name' => 'Ruben']); + + LazyData::$allowedIncludes = null; + + $response = LazyData::from('Ruben')->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); +}); + +it('can dynamically exclude data based upon the request', function () { + DefaultLazyData::$allowedExcludes = []; + + $response = DefaultLazyData::from('Ruben')->toResponse(request()); + + expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); + + DefaultLazyData::$allowedExcludes = ['name']; + + $excludedResponse = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($excludedResponse->getData(true))->toBe([]); +}); + +it('can disabled excluding data dynamically from the request', function () { + DefaultLazyData::$allowedExcludes = []; + + $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); + + DefaultLazyData::$allowedExcludes = ['name']; + + $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($response->getData(true))->toBe([]); + + DefaultLazyData::$allowedExcludes = null; + + $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($response->getData(true))->toBe([]); +}); + +it('can disabled only data dynamically from the request', function () { + OnlyData::$allowedOnly = []; + + $response = OnlyData::from([ + 'first_name' => 'Ruben', + 'last_name' => 'Van Assche', + ])->toResponse(request()->merge([ + 'only' => 'first_name', + ])); + + expect($response->getData(true))->toBe([ + 'first_name' => 'Ruben', + 'last_name' => 'Van Assche', + ]); + + OnlyData::$allowedOnly = ['first_name']; + + $response = OnlyData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ + 'only' => 'first_name', + ])); + + expect($response->getData(true))->toMatchArray([ + 'first_name' => 'Ruben', + ]); + + OnlyData::$allowedOnly = null; + + $response = OnlyData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ + 'only' => 'first_name', + ])); + + expect($response->getData(true))->toMatchArray([ + 'first_name' => 'Ruben', + ]); +}); + +it('can disabled except data dynamically from the request', function () { + ExceptData::$allowedExcept = []; + + $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ + 'except' => 'first_name', + ])); + + expect($response->getData(true))->toMatchArray([ + 'first_name' => 'Ruben', + 'last_name' => 'Van Assche', + ]); + + ExceptData::$allowedExcept = ['first_name']; + + $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ + 'except' => 'first_name', + ])); + + expect($response->getData(true))->toMatchArray([ + 'last_name' => 'Van Assche', + ]); + + ExceptData::$allowedExcept = null; + + $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ + 'except' => 'first_name', + ])); + + expect($response->getData(true))->toMatchArray([ + 'last_name' => 'Van Assche', + ]); +}); + + +it('will not include lazy optional values when transforming', function () { + $data = new class ('Hello World', Lazy::create(fn () => Optional::make())) extends Data { + public function __construct( + public string $string, + public string|Optional|Lazy $lazy_optional_string, + ) { + } + }; + + expect($data->toArray())->toMatchArray([ + 'string' => 'Hello World', + ]); +}); + +it('excludes optional values data', function () { + $dataClass = new class () extends Data { + public string|Optional $name; + }; + + $data = $dataClass::from([]); + + expect($data->toArray())->toBe([]); +}); + +it('includes value if not optional data', function () { + $dataClass = new class () extends Data { + public string|Optional $name; + }; + + $data = $dataClass::from([ + 'name' => 'Freek', + ]); + + expect($data->toArray())->toMatchArray([ + 'name' => 'Freek', + ]); +}); + + +it('can conditionally include', function () { + expect( + MultiLazyData::from(DummyDto::rick())->includeWhen('artist', false)->toArray() + )->toBeEmpty(); + + expect( + MultiLazyData::from(DummyDto::rick()) + ->includeWhen('artist', true) + ->toArray() + ) + ->toMatchArray([ + 'artist' => 'Rick Astley', + ]); + + expect( + MultiLazyData::from(DummyDto::rick()) + ->includeWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') + ->toArray() + ) + ->toMatchArray([ + 'name' => 'Never gonna give you up', + ]); +}); + +it('can conditionally include nested', function () { + $data = new class () extends Data { + public NestedLazyData $nested; + }; + + $data->nested = NestedLazyData::from('Hello World'); + + expect($data->toArray())->toMatchArray(['nested' => []]); + + expect($data->includeWhen('nested.simple', true)->toArray()) + ->toMatchArray([ + 'nested' => ['simple' => ['string' => 'Hello World']], + ]); +}); + +it('can conditionally include using class defaults', function () { + PartialClassConditionalData::setDefinitions(includeDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::createLazy(enabled: false)) + ->toArray() + ->toMatchArray(['enabled' => false]); + + expect(PartialClassConditionalData::createLazy(enabled: true)) + ->toArray() + ->toMatchArray(['enabled' => true, 'string' => 'Hello World']); +}); + +it('can conditionally include using class defaults nested', function () { + PartialClassConditionalData::setDefinitions(includeDefinitions: [ + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::createLazy(enabled: true)) + ->toArray() + ->toMatchArray(['enabled' => true, 'nested' => ['string' => 'Hello World']]); +}); + +it('can conditionally include using class defaults multiple', function () { + PartialClassConditionalData::setDefinitions(includeDefinitions: [ + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::createLazy(enabled: false)) + ->toArray() + ->toMatchArray(['enabled' => false]); + + expect(PartialClassConditionalData::createLazy(enabled: true)) + ->toArray() + ->toMatchArray([ + 'enabled' => true, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); +}); + +it('can conditionally exclude', function () { + $data = new MultiLazyData( + Lazy::create(fn () => 'Rick Astley')->defaultIncluded(), + Lazy::create(fn () => 'Never gonna give you up')->defaultIncluded(), + 1989 + ); + + expect((clone $data)->exceptWhen('artist', false)->toArray()) + ->toMatchArray([ + 'artist' => 'Rick Astley', + 'name' => 'Never gonna give you up', + 'year' => 1989, + ]); + + expect((clone $data)->exceptWhen('artist', true)->toArray()) + ->toMatchArray([ + 'name' => 'Never gonna give you up', + 'year' => 1989, + ]); + + expect( + (clone $data) + ->exceptWhen('name', fn (MultiLazyData $data) => $data->artist->resolve() === 'Rick Astley') + ->toArray() + ) + ->toMatchArray([ + 'artist' => 'Rick Astley', + 'year' => 1989, + ]); +}); + +it('can conditionally exclude nested', function () { + $data = new class () extends Data { + public NestedLazyData $nested; + }; + + $data->nested = new NestedLazyData(Lazy::create(fn () => SimpleData::from('Hello World'))->defaultIncluded()); + + expect($data->toArray())->toMatchArray([ + 'nested' => ['simple' => ['string' => 'Hello World']], + ]); + + expect($data->exceptWhen('nested.simple', true)->toArray()) + ->toMatchArray(['nested' => []]); +}); + +it('can conditionally exclude using class defaults', function () { + PartialClassConditionalData::setDefinitions(excludeDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::createDefaultIncluded(enabled: true)) + ->toArray() + ->toMatchArray([ + 'enabled' => true, + 'nested' => ['string' => 'Hello World'], + ]); +}); + +it('can conditionally exclude using class defaults nested', function () { + PartialClassConditionalData::setDefinitions(excludeDefinitions: [ + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::createDefaultIncluded(enabled: true)) + ->toArray() + ->toMatchArray([ + 'enabled' => true, + 'string' => 'Hello World', + ]); +}); + +it('can conditionally exclude using multiple class defaults', function () { + PartialClassConditionalData::setDefinitions(excludeDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::createDefaultIncluded(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::createDefaultIncluded(enabled: true)) + ->toArray() + ->toMatchArray(['enabled' => true]); +}); + +it('can conditionally define only', function () { + $data = new MultiData('Hello', 'World'); + + expect( + (clone $data)->onlyWhen('first', true)->toArray() + ) + ->toMatchArray([ + 'first' => 'Hello', + ]); + + expect( + (clone $data)->onlyWhen('first', false)->toArray() + ) + ->toMatchArray([ + 'first' => 'Hello', + 'second' => 'World', + ]); + + expect( + (clone $data) + ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') + ->toArray() + ) + ->toMatchArray(['second' => 'World']); + + expect( + (clone $data) + ->onlyWhen('first', fn (MultiData $data) => $data->first === 'Hello') + ->onlyWhen('second', fn (MultiData $data) => $data->second === 'World') + ->toArray() + ) + ->toMatchArray([ + 'first' => 'Hello', + 'second' => 'World', + ]); +}); + +it('can conditionally define only nested', function () { + $data = new class () extends Data { + public MultiData $nested; + }; + + $data->nested = new MultiData('Hello', 'World'); + + expect( + (clone $data)->onlyWhen('nested.first', true)->toArray() + )->toMatchArray([ + 'nested' => ['first' => 'Hello'], + ]); + + expect( + (clone $data)->onlyWhen('nested.{first, second}', true)->toArray() + )->toMatchArray([ + 'nested' => [ + 'first' => 'Hello', + 'second' => 'World', + ], + ]); +}); + +it('can conditionally define only using class defaults', function () { + PartialClassConditionalData::setDefinitions(onlyDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::create(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::create(enabled: true)) + ->toArray() + ->toMatchArray(['string' => 'Hello World']); +}); + +it('can conditionally define only using class defaults nested', function () { + PartialClassConditionalData::setDefinitions(onlyDefinitions: [ + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::create(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::create(enabled: true)) + ->toArray() + ->toMatchArray([ + 'nested' => ['string' => 'Hello World'], + ]); +}); + +it('can conditionally define only using multiple class defaults', function () { + PartialClassConditionalData::setDefinitions(onlyDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::create(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::create(enabled: true)) + ->toArray() + ->toMatchArray([ + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); +}); + +it('can conditionally define except', function () { + $data = new MultiData('Hello', 'World'); + + expect((clone $data)->exceptWhen('first', true)) + ->toArray() + ->toMatchArray(['second' => 'World']); + + expect((clone $data)->exceptWhen('first', false)) + ->toArray() + ->toMatchArray([ + 'first' => 'Hello', + 'second' => 'World', + ]); + + expect( + (clone $data) + ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') + ) + ->toArray() + ->toMatchArray([ + 'first' => 'Hello', + ]); + + expect( + (clone $data) + ->exceptWhen('first', fn (MultiData $data) => $data->first === 'Hello') + ->exceptWhen('second', fn (MultiData $data) => $data->second === 'World') + ->toArray() + )->toBeEmpty(); +}); + +it('can conditionally define except nested', function () { + $data = new class () extends Data { + public MultiData $nested; + }; + + $data->nested = new MultiData('Hello', 'World'); + + expect((clone $data)->exceptWhen('nested.first', true)) + ->toArray() + ->toMatchArray(['nested' => ['second' => 'World']]); + + expect((clone $data)->exceptWhen('nested.{first, second}', true)) + ->toArray() + ->toMatchArray(['nested' => []]); +}); + +it('can conditionally define except using class defaults', function () { + PartialClassConditionalData::setDefinitions(exceptDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::create(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::create(enabled: true)) + ->toArray() + ->toMatchArray([ + 'enabled' => true, + 'nested' => ['string' => 'Hello World'], + ]); +}); + +it('can conditionally define except using class defaults nested', function () { + PartialClassConditionalData::setDefinitions(exceptDefinitions: [ + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::create(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::create(enabled: true)) + ->toArray() + ->toMatchArray([ + 'enabled' => true, + 'string' => 'Hello World', + 'nested' => [], + ]); +}); + +it('can conditionally define except using multiple class defaults', function () { + PartialClassConditionalData::setDefinitions(exceptDefinitions: [ + 'string' => fn (PartialClassConditionalData $data) => $data->enabled, + 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, + ]); + + expect(PartialClassConditionalData::create(enabled: false)) + ->toArray() + ->toMatchArray([ + 'enabled' => false, + 'string' => 'Hello World', + 'nested' => ['string' => 'Hello World'], + ]); + + expect(PartialClassConditionalData::create(enabled: true)) + ->toArray() + ->toMatchArray([ + 'enabled' => true, + 'nested' => [], + ]); +}); + +test('only has precedence over except', function () { + $data = new MultiData('Hello', 'World'); + + expect( + (clone $data)->onlyWhen('first', true) + ->exceptWhen('first', true) + ->toArray() + )->toMatchArray(['second' => 'World']); + + expect( + (clone $data)->exceptWhen('first', true)->onlyWhen('first', true)->toArray() + )->toMatchArray(['second' => 'World']); +}); + +it('can perform only and except on array properties', function () { + $data = new class ('Hello World', ['string' => 'Hello World', 'int' => 42]) extends Data { + public function __construct( + public string $string, + public array $array + ) { + } + }; + + expect((clone $data)->only('string', 'array.int')) + ->toArray() + ->toMatchArray([ + 'string' => 'Hello World', + 'array' => ['int' => 42], + ]); + + expect((clone $data)->except('string', 'array.int')) + ->toArray() + ->toMatchArray([ + 'array' => ['string' => 'Hello World'], + ]); +}); + + +it('can fetch lazy union data', function () { + $data = UnionData::from(1); + + expect($data->id)->toBe(1); + expect($data->simple->string)->toBe('A'); + expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); + expect($data->fakeModel->string)->toBe('lazy'); +}); + +it('can fetch non-lazy union data', function () { + $data = UnionData::from('A'); + + expect($data->id)->toBe(1); + expect($data->simple->string)->toBe('A'); + expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); + expect($data->fakeModel->string)->toBe('non-lazy'); +}); + + +it('has array access and will replicate partialtrees (collection)', function () { + $collection = MultiData::collect([ + new MultiData('first', 'second'), + ], DataCollection::class)->only('second'); + + expect($collection[0]->toArray())->toEqual(['second' => 'second']); +}); + +it('can dynamically include data based upon the request (collection)', function () { + LazyData::$allowedIncludes = ['']; + + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); + + expect($response)->getData(true) + ->toMatchArray([ + [], + [], + [], + ]); + + LazyData::$allowedIncludes = ['name']; + + $includedResponse = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($includedResponse)->getData(true) + ->toMatchArray([ + ['name' => 'Ruben'], + ['name' => 'Freek'], + ['name' => 'Brent'], + ]); +}); + +it('can disabled manually including data in the request (collection)', function () { + LazyData::$allowedIncludes = []; + + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($response)->getData(true) + ->toMatchArray([ + [], + [], + [], + ]); + + LazyData::$allowedIncludes = ['name']; + + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($response)->getData(true) + ->toMatchArray([ + ['name' => 'Ruben'], + ['name' => 'Freek'], + ['name' => 'Brent'], + ]); + + LazyData::$allowedIncludes = null; + + $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'include' => 'name', + ])); + + expect($response)->getData(true) + ->toMatchArray([ + ['name' => 'Ruben'], + ['name' => 'Freek'], + ['name' => 'Brent'], + ]); +}); + +it('can dynamically exclude data based upon the request (collection)', function () { + DefaultLazyData::$allowedExcludes = []; + + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); + + expect($response)->getData(true) + ->toMatchArray([ + ['name' => 'Ruben'], + ['name' => 'Freek'], + ['name' => 'Brent'], + ]); + + DefaultLazyData::$allowedExcludes = ['name']; + + $excludedResponse = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($excludedResponse)->getData(true) + ->toMatchArray([ + [], + [], + [], + ]); +}); + +it('can disable manually excluding data in the request (collection)', function () { + DefaultLazyData::$allowedExcludes = []; + + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($response)->getData(true) + ->toMatchArray([ + ['name' => 'Ruben'], + ['name' => 'Freek'], + ['name' => 'Brent'], + ]); + + DefaultLazyData::$allowedExcludes = ['name']; + + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($response)->getData(true) + ->toMatchArray([ + [], + [], + [], + ]); + + DefaultLazyData::$allowedExcludes = null; + + $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ + 'exclude' => 'name', + ])); + + expect($response)->getData(true) + ->toMatchArray([ + [], + [], + [], + ]); +}); From 1cab4a5a9221b2bb92bd2396ef4c0f5f5d68f455 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 21 Sep 2023 17:12:34 +0200 Subject: [PATCH 030/124] wip --- tests/PartialsTest.php | 58 ++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 31 deletions(-) diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index c49fd2b2..0e5a973a 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -26,7 +26,7 @@ it('can include a lazy property', function () { $data = new LazyData(Lazy::create(fn () => 'test')); -// expect($data->toArray())->toBe([]); + expect($data->toArray())->toBe([]); expect($data->include('name')->toArray()) ->toMatchArray([ @@ -66,39 +66,35 @@ public function __construct( expect((clone $data)->toArray())->toBe([]); - expect((clone $data)->include('data')->toArray()) - ->toMatchArray([ - 'data' => [], - ]); + expect((clone $data)->include('data')->toArray())->toMatchArray([ + 'data' => [], + ]); - expect((clone $data)->include('data.name')->toArray()) - ->toMatchArray([ - 'data' => ['name' => 'Hello'], - ]); + expect((clone $data)->include('data.name')->toArray())->toMatchArray([ + 'data' => ['name' => 'Hello'], + ]); - expect((clone $data)->include('collection')->toArray()) - ->toMatchArray([ - 'collection' => [ - [], - [], - [], - [], - [], - [], - ], - ]); + expect((clone $data)->include('collection')->toArray())->toMatchArray([ + 'collection' => [ + [], + [], + [], + [], + [], + [], + ], + ]); - expect((clone $data)->include('collection.name')->toArray()) - ->toMatchArray([ - 'collection' => [ - ['name' => 'is'], - ['name' => 'it'], - ['name' => 'me'], - ['name' => 'your'], - ['name' => 'looking'], - ['name' => 'for'], - ], - ]); + expect((clone $data)->include('collection.name')->toArray())->toMatchArray([ + 'collection' => [ + ['name' => 'is'], + ['name' => 'it'], + ['name' => 'me'], + ['name' => 'your'], + ['name' => 'looking'], + ['name' => 'for'], + ], + ]); }); it('can include specific nested data', function () { From 603f8b178944d6e238ce72e059e061d0c2a1f740 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 21 Sep 2023 15:13:07 +0000 Subject: [PATCH 031/124] Fix styling --- tests/DataCollectionTest.php | 2 -- tests/DataTest.php | 14 -------------- 2 files changed, 16 deletions(-) diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 5cf142d2..d4292f76 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -13,9 +13,7 @@ use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; -use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; use Spatie\LaravelData\Tests\Fakes\LazyData; -use Spatie\LaravelData\Tests\Fakes\MultiData; use Spatie\LaravelData\Tests\Fakes\SimpleData; use function Spatie\Snapshots\assertMatchesJsonSnapshot; diff --git a/tests/DataTest.php b/tests/DataTest.php index 96fa41f2..8c767898 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -7,7 +7,6 @@ use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Collection; use Illuminate\Validation\ValidationException; -use Inertia\LazyProp; use Spatie\LaravelData\Attributes\Computed; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\Hidden; @@ -34,8 +33,6 @@ use Spatie\LaravelData\Exceptions\CannotSetComputedValue; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; -use Spatie\LaravelData\Support\Lazy\ClosureLazy; -use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Fakes\Castables\SimpleCastable; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCast; @@ -44,23 +41,13 @@ use Spatie\LaravelData\Tests\Fakes\Casts\StringToUpperCast; use Spatie\LaravelData\Tests\Fakes\CircData; use Spatie\LaravelData\Tests\Fakes\DataWithMapper; -use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; -use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; -use Spatie\LaravelData\Tests\Fakes\ExceptData; -use Spatie\LaravelData\Tests\Fakes\FakeModelData; -use Spatie\LaravelData\Tests\Fakes\FakeNestedModelData; use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; -use Spatie\LaravelData\Tests\Fakes\Models\FakeNestedModel; use Spatie\LaravelData\Tests\Fakes\MultiData; -use Spatie\LaravelData\Tests\Fakes\MultiLazyData; use Spatie\LaravelData\Tests\Fakes\MultiNestedData; use Spatie\LaravelData\Tests\Fakes\NestedData; -use Spatie\LaravelData\Tests\Fakes\NestedLazyData; -use Spatie\LaravelData\Tests\Fakes\OnlyData; -use Spatie\LaravelData\Tests\Fakes\PartialClassConditionalData; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedProperty; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithoutConstructor; @@ -69,7 +56,6 @@ use Spatie\LaravelData\Tests\Fakes\Transformers\ConfidentialDataTransformer; use Spatie\LaravelData\Tests\Fakes\Transformers\StringToUpperTransformer; use Spatie\LaravelData\Tests\Fakes\UlarData; -use Spatie\LaravelData\Tests\Fakes\UnionData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; use Spatie\LaravelData\WithData; From d13e532c6b22bec41fe5825815237fe76e9065c8 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 22 Sep 2023 14:58:03 +0200 Subject: [PATCH 032/124] wip --- UPGRADING.md | 22 ++ docs/advanced-usage/_index.md | 2 +- .../get-data-from-a-class-quickly.md | 54 +++ docs/advanced-usage/validation-attributes.md | 197 +++++------ docs/as-a-data-transfer-object/casts.md | 2 +- docs/as-a-data-transfer-object/collections.md | 223 ++++++++----- docs/as-a-data-transfer-object/computed.md | 3 +- .../creating-a-data-object.md | 111 +------ docs/as-a-data-transfer-object/defaults.md | 2 +- .../mapping-property-names.md | 52 +++ docs/as-a-data-transfer-object/nesting.md | 121 ++++++- .../optional-properties.md | 2 +- .../request-to-data-object.md | 29 +- docs/as-a-resource/_index.md | 2 +- docs/validation/_index.md | 4 + docs/validation/auto-rule-inferring.md | 60 ++++ docs/validation/introduction.md | 309 ++++++++++++++++++ docs/validation/manual-rules.md | 193 +++++++++++ docs/validation/nesting-data.md | 6 + docs/validation/skipping-validation.md | 62 ++++ .../validation/using-validation-attributes.md | 168 ++++++++++ docs/validation/working-with-the-validator.md | 162 +++++++++ ...php => WithDeprecatedCollectionMethod.php} | 2 +- tests/DataCollectionTest.php | 51 ++- tests/DataTest.php | 36 -- tests/Fakes/Collections/CustomCollection.php | 10 + tests/Fakes/UnionData.php | 45 --- tests/PartialsTest.php | 235 +++++++++---- 28 files changed, 1696 insertions(+), 469 deletions(-) create mode 100644 docs/advanced-usage/get-data-from-a-class-quickly.md create mode 100644 docs/as-a-data-transfer-object/mapping-property-names.md create mode 100644 docs/validation/_index.md create mode 100644 docs/validation/auto-rule-inferring.md create mode 100644 docs/validation/introduction.md create mode 100644 docs/validation/manual-rules.md create mode 100644 docs/validation/nesting-data.md create mode 100644 docs/validation/skipping-validation.md create mode 100644 docs/validation/using-validation-attributes.md create mode 100644 docs/validation/working-with-the-validator.md rename src/Concerns/{DeprecatedData.php => WithDeprecatedCollectionMethod.php} (97%) create mode 100644 tests/Fakes/Collections/CustomCollection.php delete mode 100644 tests/Fakes/UnionData.php diff --git a/UPGRADING.md b/UPGRADING.md index 93a6cac9..efb03726 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,28 @@ Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. +## From v3 to v4 + +The following things are required when upgrading: + +- Start by going through your code and replace all static `SomeData::collection($items)` method calls with `SomeData::collect($items, DataCollection::class)` + - Use `DataPaginatedCollection::class` when you're expecting a paginated collection + - Use `DataCursorPaginatedCollection::class` when you're expecting a cursor paginated collection + - For a more gentle upgrade you can also use the `WithDeprecatedCollectionMethod` trait which adds the collection method again, but this trait will be removed in v5 + - If you were using `$_collectionClass`, `$_paginatedCollectionClass` or `$_cursorPaginatedCollectionClass` then take a look at the magic collect functionality on information about how to replace these +- If you were manually working with `$_includes`, ` $_excludes`, `$_only`, `$_except` or `$_wrap` these can now be found within the `$_dataContext` +- We split up some traits and interfaces, if you're manually using these on you own data implementation then take a look what has changed + - DataTrait (T) and PrepareableData (T/I) were removed + - 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 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 + +We advise you to take a look at the following things: +- Take a look within your data objects if `DataCollection`'s, `DataPaginatedCollection`'s and `DataCursorPaginatedCollection`'s can be replaced with regular arrays, Laravel Collections and Paginator +- Replace `DataCollectionOf` attributes with annotations, providing IDE completion and more info for static analyzers +- Replace some `extends Data` definitions with `extends Resource` or `extends Dto` for more minimal data objects ## From v2 to v3 Upgrading to laravel data shouldn't take long, we've documented all possible changes just to provide the whole context. You probably won't have to do anything: diff --git a/docs/advanced-usage/_index.md b/docs/advanced-usage/_index.md index 5303d653..12ccdad9 100644 --- a/docs/advanced-usage/_index.md +++ b/docs/advanced-usage/_index.md @@ -1,4 +1,4 @@ --- title: Advanced usage -weight: 4 +weight: 5 --- diff --git a/docs/advanced-usage/get-data-from-a-class-quickly.md b/docs/advanced-usage/get-data-from-a-class-quickly.md new file mode 100644 index 00000000..2887c2ec --- /dev/null +++ b/docs/advanced-usage/get-data-from-a-class-quickly.md @@ -0,0 +1,54 @@ +--- +title: Get data from a class quickly +weight: 15 +--- + +By adding the `WithData` trait to a Model, Request or any class that can be magically be converted to a data object, +you'll enable support for the `getData` method. This method will automatically generate a data object for the object it +is called upon. + +For example, let's retake a look at the `Song` model we saw earlier. We can add the `WithData` trait as follows: + +```php +class Song extends Model{ + use WithData; + + protected $dataClass = SongData::class; +} +``` + +Now we can quickly get the data object for the model as such: + +```php +Song::firstOrFail($id)->getData(); // A SongData object +``` + +We can do the same with a FormRequest, we don't use a property here to define the data class but use a method instead: + +```php +class SongRequest extends FormRequest +{ + use WithData; + + protected function dataClass(): string + { + return SongData::class; + } +} +``` + +Now within a controller where the request is injected, we can get the data object like this: + +```php +class SongController +{ + public function __invoke(SongRequest $request): SongData + { + $data = $request->getData(); + + $song = Song::create($data); + + return $data; + } +} +``` diff --git a/docs/advanced-usage/validation-attributes.md b/docs/advanced-usage/validation-attributes.md index 578db49e..a231e852 100644 --- a/docs/advanced-usage/validation-attributes.md +++ b/docs/advanced-usage/validation-attributes.md @@ -1,46 +1,11 @@ --- title: Validation attributes -weight: 14 +weight: 16 --- -It is possible to validate the request before a data object is constructed. This can be done by adding validation attributes to the properties of a data object like this: +These are all the validation attributes currently available in laravel-data. -```php -class SongData extends Data -{ - public function __construct( - #[Uuid()] - public string $uuid, - #[Max(15), IP, StartsWith('192.')] - public string $ip, - ) { - } -} -``` - -## Creating your validation attribute - -It is possible to create your own validation attribute by extending the `CustomValidationAttribute` class, this class has a `getRules` method that returns the rules that should be applied to the property. - -```php -#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] -class CustomRule extends CustomValidationAttribute -{ - /** - * @return array|object|string - */ - public function getRules(ValidationPath $path): array|object|string; - { - return [new CustomRule()]; - } -} -``` - -Quick note: you can only use these rules as an attribute, not as a class rule within the static `rules` method of the data class. - -## Available validation attributes - -### Accepted +## Accepted [Docs](https://laravel.com/docs/9.x/validation#rule-accepted) @@ -49,7 +14,7 @@ Quick note: you can only use these rules as an attribute, not as a class rule wi public bool $closure; ``` -### AcceptedIf +## AcceptedIf [Docs](https://laravel.com/docs/9.x/validation#rule-accepted-if) @@ -58,7 +23,7 @@ public bool $closure; public bool $closure; ``` -### ActiveUrl +## ActiveUrl [Docs](https://laravel.com/docs/9.x/validation#rule-active-url) @@ -67,7 +32,7 @@ public bool $closure; public string $closure; ``` -### After +## After [Docs](https://laravel.com/docs/9.x/validation#rule-after) @@ -83,7 +48,7 @@ public Carbon $closure; public Carbon $closure; ``` -### AfterOrEqual +## AfterOrEqual [Docs](https://laravel.com/docs/9.x/validation#rule-after-or-equal) @@ -99,7 +64,7 @@ public Carbon $closure; public Carbon $closure; ``` -### Alpha +## Alpha [Docs](https://laravel.com/docs/9.x/validation#rule-alpha) @@ -108,7 +73,7 @@ public Carbon $closure; public string $closure; ``` -### AlphaDash +## AlphaDash [Docs](https://laravel.com/docs/9.x/validation#rule-alpha-dash) @@ -117,7 +82,7 @@ public string $closure; public string $closure; ``` -### AlphaNumeric +## AlphaNumeric [Docs](https://laravel.com/docs/9.x/validation#rule-alpha-num) @@ -126,7 +91,7 @@ public string $closure; public string $closure; ``` -### ArrayType +## ArrayType [Docs](https://laravel.com/docs/9.x/validation#rule-array) @@ -141,7 +106,7 @@ public array $closure; public array $closure; ``` -### Bail +## Bail [Docs](https://laravel.com/docs/9.x/validation#rule-bail) @@ -150,7 +115,7 @@ public array $closure; public string $closure; ``` -### Before +## Before [Docs](https://laravel.com/docs/9.x/validation#rule-before) @@ -166,7 +131,7 @@ public Carbon $closure; public Carbon $closure; ``` -### BeforeOrEqual +## BeforeOrEqual [Docs](https://laravel.com/docs/9.x/validation#rule-before-or-equal) @@ -182,7 +147,7 @@ public Carbon $closure; public Carbon $closure; ``` -### Between +## Between [Docs](https://laravel.com/docs/9.x/validation#rule-between) @@ -191,7 +156,7 @@ public Carbon $closure; public int $closure; ``` -### BooleanType +## BooleanType [Docs](https://laravel.com/docs/9.x/validation#rule-boolean) @@ -200,7 +165,7 @@ public int $closure; public bool $closure; ``` -### Confirmed +## Confirmed [Docs](https://laravel.com/docs/9.x/validation#rule-confirmed) @@ -209,7 +174,7 @@ public bool $closure; public string $closure; ``` -### CurrentPassword +## CurrentPassword [Docs](https://laravel.com/docs/9.x/validation#rule-current-password) @@ -221,7 +186,7 @@ public string $closure; public string $closure; ``` -### Date +## Date [Docs](https://laravel.com/docs/9.x/validation#rule-date) @@ -230,7 +195,7 @@ public string $closure; public Carbon $closure; ``` -### DateEquals +## DateEquals [Docs](https://laravel.com/docs/9.x/validation#rule-date-equals) @@ -242,7 +207,7 @@ public Carbon $closure; public Carbon $closure; ``` -### DateFormat +## DateFormat [Docs](https://laravel.com/docs/9.x/validation#rule-date-format) @@ -251,7 +216,7 @@ public Carbon $closure; public Carbon $closure; ``` -### Different +## Different [Docs](https://laravel.com/docs/9.x/validation#rule-different) @@ -260,7 +225,7 @@ public Carbon $closure; public string $closure; ``` -### Digits +## Digits [Docs](https://laravel.com/docs/9.x/validation#rule-digits) @@ -269,7 +234,7 @@ public string $closure; public int $closure; ``` -### DigitsBetween +## DigitsBetween [Docs](https://laravel.com/docs/9.x/validation#rule-digits-between) @@ -278,7 +243,7 @@ public int $closure; public int $closure; ``` -### Dimensions +## Dimensions [Docs](https://laravel.com/docs/9.x/validation#rule-dimensions) @@ -290,7 +255,7 @@ public UploadedFile $closure; public UploadedFile $closure; ``` -### Distinct +## Distinct [Docs](https://laravel.com/docs/9.x/validation#rule-distinct) @@ -305,7 +270,7 @@ public string $closure; public string $closure; ``` -### Email +## Email [Docs](https://laravel.com/docs/9.x/validation#rule-email) @@ -323,7 +288,7 @@ public string $closure; public string $closure; ``` -### EndsWith +## EndsWith [Docs](https://laravel.com/docs/9.x/validation#rule-ends-with) @@ -338,7 +303,7 @@ public string $closure; public string $closure; ``` -### Enum +## Enum [Docs](https://laravel.com/docs/9.x/validation#rule-enum) @@ -347,7 +312,7 @@ public string $closure; public string $closure; ``` -### ExcludeIf +## ExcludeIf *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -358,7 +323,7 @@ public string $closure; public string $closure; ``` -### ExcludeUnless +## ExcludeUnless *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -369,7 +334,7 @@ public string $closure; public string $closure; ``` -### ExcludeWithout +## ExcludeWithout *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -380,7 +345,7 @@ public string $closure; public string $closure; ``` -### Exists +## Exists [Docs](https://laravel.com/docs/9.x/validation#rule-exists) @@ -401,7 +366,7 @@ public string $closure; public string $closure; ``` -### File +## File [Docs](https://laravel.com/docs/9.x/validation#rule-file) @@ -410,7 +375,7 @@ public string $closure; public UploadedFile $closure; ``` -### Filled +## Filled [Docs](https://laravel.com/docs/9.x/validation#rule-filled) @@ -419,7 +384,7 @@ public UploadedFile $closure; public string $closure; ``` -### GreaterThan +## GreaterThan [Docs](https://laravel.com/docs/9.x/validation#rule-gt) @@ -428,7 +393,7 @@ public string $closure; public int $closure; ``` -### GreaterThanOrEqualTo +## GreaterThanOrEqualTo [Docs](https://laravel.com/docs/9.x/validation#rule-gte) @@ -437,7 +402,7 @@ public int $closure; public int $closure; ``` -### Image +## Image [Docs](https://laravel.com/docs/9.x/validation#rule-image) @@ -446,7 +411,7 @@ public int $closure; public UploadedFile $closure; ``` -### In +## In [Docs](https://laravel.com/docs/9.x/validation#rule-in) @@ -458,7 +423,7 @@ public mixed $closure; public mixed $closure; ``` -### InArray +## InArray [Docs](https://laravel.com/docs/9.x/validation#rule-in-array) @@ -467,7 +432,7 @@ public mixed $closure; public string $closure; ``` -### IntegerType +## IntegerType [Docs](https://laravel.com/docs/9.x/validation#rule-integer) @@ -476,7 +441,7 @@ public string $closure; public int $closure; ``` -### IP +## IP [Docs](https://laravel.com/docs/9.x/validation#rule-ip) @@ -485,7 +450,7 @@ public int $closure; public string $closure; ``` -### IPv4 +## IPv4 [Docs](https://laravel.com/docs/9.x/validation#rule-ipv4) @@ -494,7 +459,7 @@ public string $closure; public string $closure; ``` -### IPv6 +## IPv6 [Docs](https://laravel.com/docs/9.x/validation#rule-ipv6) @@ -503,7 +468,7 @@ public string $closure; public string $closure; ``` -### Json +## Json [Docs](https://laravel.com/docs/9.x/validation#rule-json) @@ -512,7 +477,7 @@ public string $closure; public string $closure; ``` -### LessThan +## LessThan [Docs](https://laravel.com/docs/9.x/validation#rule-lt) @@ -521,7 +486,7 @@ public string $closure; public int $closure; ``` -### LessThanOrEqualTo +## LessThanOrEqualTo [Docs](https://laravel.com/docs/9.x/validation#rule-lte) @@ -530,7 +495,7 @@ public int $closure; public int $closure; ``` -### Max +## Max [Docs](https://laravel.com/docs/9.x/validation#rule-max) @@ -539,7 +504,7 @@ public int $closure; public int $closure; ``` -### MimeTypes +## MimeTypes [Docs](https://laravel.com/docs/9.x/validation#rule-mimetypes) @@ -554,7 +519,7 @@ public UploadedFile $closure; public UploadedFile $closure; ``` -### Mimes +## Mimes [Docs](https://laravel.com/docs/9.x/validation#rule-mimes) @@ -569,7 +534,7 @@ public UploadedFile $closure; public UploadedFile $closure; ``` -### Min +## Min [Docs](https://laravel.com/docs/9.x/validation#rule-min) @@ -578,7 +543,7 @@ public UploadedFile $closure; public int $closure; ``` -### MultipleOf +## MultipleOf [Docs](https://laravel.com/docs/9.x/validation#rule-multiple-of) @@ -587,7 +552,7 @@ public int $closure; public int $closure; ``` -### NotIn +## NotIn [Docs](https://laravel.com/docs/9.x/validation#rule-not-in) @@ -599,7 +564,7 @@ public mixed $closure; public mixed $closure; ``` -### NotRegex +## NotRegex [Docs](https://laravel.com/docs/9.x/validation#rule-not-regex) @@ -608,7 +573,7 @@ public mixed $closure; public string $closure; ``` -### Nullable +## Nullable [Docs](https://laravel.com/docs/9.x/validation#rule-nullable) @@ -617,7 +582,7 @@ public string $closure; public ?string $closure; ``` -### Numeric +## Numeric [Docs](https://laravel.com/docs/9.x/validation#rule-numeric) @@ -626,7 +591,7 @@ public ?string $closure; public ?string $closure; ``` -### Password +## Password [Docs](https://laravel.com/docs/9.x/validation#rule-password) @@ -635,7 +600,7 @@ public ?string $closure; public string $closure; ``` -### Present +## Present [Docs](https://laravel.com/docs/9.x/validation#rule-present) @@ -644,7 +609,7 @@ public string $closure; public string $closure; ``` -### Prohibited +## Prohibited [Docs](https://laravel.com/docs/9.x/validation#rule-prohibited) @@ -653,7 +618,7 @@ public string $closure; public ?string $closure; ``` -### ProhibitedIf +## ProhibitedIf [Docs](https://laravel.com/docs/9.x/validation#rule-prohibited-if) @@ -665,7 +630,7 @@ public ?string $closure; public ?string $closure; ``` -### ProhibitedUnless +## ProhibitedUnless [Docs](https://laravel.com/docs/9.x/validation#rule-prohibited-unless) @@ -677,7 +642,7 @@ public ?string $closure; public ?string $closure; ``` -### Prohibits +## Prohibits [Docs](https://laravel.com/docs/9.x/validation#rule-prohibits) @@ -692,7 +657,7 @@ public ?string $closure; public ?string $closure; ``` -### Regex +## Regex [Docs](https://laravel.com/docs/9.x/validation#rule-regex) @@ -701,7 +666,7 @@ public ?string $closure; public string $closure; ``` -### Required +## Required [Docs](https://laravel.com/docs/9.x/validation#rule-required) @@ -710,7 +675,7 @@ public string $closure; public string $closure; ``` -### RequiredIf +## RequiredIf [Docs](https://laravel.com/docs/9.x/validation#rule-required-if) @@ -722,7 +687,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredUnless +## RequiredUnless [Docs](https://laravel.com/docs/9.x/validation#rule-required-unless) @@ -734,7 +699,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWith +## RequiredWith [Docs](https://laravel.com/docs/9.x/validation#rule-required-with) @@ -749,7 +714,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWithAll +## RequiredWithAll [Docs](https://laravel.com/docs/9.x/validation#rule-required-with-all) @@ -764,7 +729,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWithout +## RequiredWithout [Docs](https://laravel.com/docs/9.x/validation#rule-required-without) @@ -779,7 +744,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWithoutAll +## RequiredWithoutAll [Docs](https://laravel.com/docs/9.x/validation#rule-required-without-all) @@ -794,7 +759,7 @@ public ?string $closure; public ?string $closure; ``` -### Rule +## Rule ```php #[Rule('string|uuid')] @@ -804,7 +769,7 @@ public string $closure; public string $closure; ``` -### Same +## Same [Docs](https://laravel.com/docs/9.x/validation#rule-same) @@ -813,7 +778,7 @@ public string $closure; public string $closure; ``` -### Size +## Size [Docs](https://laravel.com/docs/9.x/validation#rule-size) @@ -822,7 +787,7 @@ public string $closure; public string $closure; ``` -### Sometimes +## Sometimes [Docs](https://laravel.com/docs/9.x/validation#validating-when-present) @@ -831,7 +796,7 @@ public string $closure; public string $closure; ``` -### StartsWith +## StartsWith [Docs](https://laravel.com/docs/9.x/validation#rule-starts-with) @@ -846,7 +811,7 @@ public string $closure; public string $closure; ``` -### StringType +## StringType [Docs](https://laravel.com/docs/9.x/validation#rule-string) @@ -855,7 +820,7 @@ public string $closure; public string $closure; ``` -### TimeZone +## TimeZone [Docs](https://laravel.com/docs/9.x/validation#rule-timezone) @@ -864,7 +829,7 @@ public string $closure; public string $closure; ``` -### Unique +## Unique [Docs](https://laravel.com/docs/9.x/validation#rule-unique) @@ -888,7 +853,7 @@ public string $closure; public string $closure; ``` -### Url +## Url [Docs](https://laravel.com/docs/9.x/validation#rule-url) @@ -897,7 +862,7 @@ public string $closure; public string $closure; ``` -### Ulid +## Ulid [Docs](https://laravel.com/docs/9.x/validation#rule-ulid) @@ -906,7 +871,7 @@ public string $closure; public string $closure; ``` -### Uuid +## Uuid [Docs](https://laravel.com/docs/9.x/validation#rule-uuid) diff --git a/docs/as-a-data-transfer-object/casts.md b/docs/as-a-data-transfer-object/casts.md index 435bb5ad..77b6dd6b 100644 --- a/docs/as-a-data-transfer-object/casts.md +++ b/docs/as-a-data-transfer-object/casts.md @@ -1,6 +1,6 @@ --- title: Casts -weight: 5 +weight: 4 --- We extend our example data object just a little bit: diff --git a/docs/as-a-data-transfer-object/collections.md b/docs/as-a-data-transfer-object/collections.md index 6ca8f683..242ed4e6 100644 --- a/docs/as-a-data-transfer-object/collections.md +++ b/docs/as-a-data-transfer-object/collections.md @@ -1,148 +1,188 @@ --- title: Collections -weight: 4 +weight: 3 --- -The package provides next to the `Data` class also a `DataCollection`, `PaginatedCollection` and `CursorPaginatedCollection` class. This collection can store a set of data objects, and we advise you to use it when storing a collection of data objects within a data object. - -For example: +It is possible to create a collection of data objects by using the `collect` method: ```php -class AlbumData extends Data -{ - public function __construct( - public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, - ) { - } -} +SongData::collect([ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], +]); // returns an array of SongData objects ``` -Using specific data collections is required for internal state management within the data object, which will become clear in the following chapters. - -## Creating `DataCollection`s +Whatever type of collection you pass in, the package will return the same type of collection with the freshly created data objects within it. As long as this type is an array, Laravel collection or paginator or a class extending from it. -There are a few different ways to create a `DataCollection`: +This opens up possibilities to create collections of Eloquent models: ```php -SongData::collection([ - ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], - ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], -]); +SongData::collect(Song::all()); // return an Eloquent collection of SongData objects ``` -If you have a collection of models, you can do the following: +Or use a paginator: ```php -SongData::collection(Song::all()); +SongData::collect(Song::paginate()); // return a LengthAwarePaginator of SongData objects + +// or + +SongData::collect(Song::cursorPaginate()); // return a CursorPaginator of SongData objects ``` -It is even possible to add a collection of data objects: +Internally the `from` method of the data class will be used to create a new data object for each item in the collection. + +When the collection already exists of data objects, the `collect` method will return the same collection: ```php -SongData::collection([ +SongData::collect([ SongData::from(['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley']), SongData::from(['title' => 'Giving Up on Love', 'artist' => 'Rick Astley']), -]); +]); // returns an array of SongData objects ``` -A `DataCollection` just works like a regular array: +The collect method also allows you to cast collections from one type into another. For example, you can pass in an `array`and get back a Laravel collection: ```php -$collection = SongData::collection([ - SongData::from(['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley']) -]); +SongData::collect($songs, Collection::class); // returns a Laravel collection of SongData objects +``` -// Count the amount of items in the collection -count($collection); +This transformation will only work with non paginator collections. -// Changing an item in the collection -$collection[0]->title = 'Giving Up on Love'; +## Magically creating collections -// Adding an item to the collection -$collection[] = SongData::from(['title' => 'Never Knew Love', 'artist' => 'Rick Astley']); +We've already seen that `from` can create data objects magically. It is also possible to create a collection of data objects magically when using `collect`. -// Removing an item from the collection -unset($collection[0]); +Let's say you've implemented a custom collection class called `SongCollection`: + +```php +class SongCollection extends Collection +{ + public function __construct( + $items = [], + public array $artists = [], + ) { + parent::__construct($items); + } +} ``` -It is even possible to loop over it with a foreach: +Since the constructor of this collection requires an extra property it cannot be created automatically. However, it is possible to define a custom collect method which can create it: ```php -foreach ($songs as $song){ - echo $song->title; +class SongData extends Data +{ + public string $title; + public string $artist; + + public static function collectArray(array $items): SongCollection + { + return new SongCollection( + parent::collect($items), + array_unique(array_map(fn(SongData $song) => $song->artist, $items)) + ); + } } ``` -## Paginated collections - -It is also possible to pass in a paginated collection: +Now when collecting an array data objects a `SongCollection` will be returned: ```php -SongData::collection(Song::paginate()); +SongData::collect([ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Living on a prayer', 'artist' => 'Bon Jovi'], +]); // returns an SongCollection of SongData objects ``` -This will return a `PaginatedDataCollection` instead of a `DataCollection`. +There are a few requirements for this to work: + +- The method must be **static and public** +- The method must **start with collect** +- The method cannot be called **collect** +- A **return type** must be defined -A cursor paginated collection can also be used: +## DataCollection's, PaginatedDataCollection's and CursorPaginatedCollection's + +The package also provides a few collection classes which can be used to create collections of data objects, it was a requirement to use these classes in the past versions of the package when nesting data objects collections in data objects. This is no longer the case and there are still valid use cases for them. + +You can create a DataCollection like this: ```php -SongData::collection(Song::cursorPaginate()); +use Spatie\LaravelData\DataCollection; + +SongData::collect(Song::all(), DataCollection::class); ``` -This will result into a `CursorPaginatedCollection` +A PaginatedDataCollection can be created like this: + +```php +use Spatie\LaravelData\PaginatedDataCollection; -## Typing data within your collections +SongData::collect(Song::paginate(), PaginatedDataCollection::class); +```` -When nesting a data collection into your data object, always type the kind of data objects that will be stored within the collection: +And a CursorPaginatedCollection can be created like this: ```php -class AlbumData extends Data +use Spatie\LaravelData\CursorPaginatedCollection; + +SongData::collect(Song::cursorPaginate(), CursorPaginatedCollection::class); +``` + +### Why using these collection classes? + +We advise you to always use arrays, Laravel collections and paginators within your data objects. But let's say you have a controller like this: + +```php +class SongController { - public function __construct( - public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, - ) { + public function index() + { + return SongData::collect(Song::all()); } } ``` -Because we typed `$songs` as `SongData`, the package automatically knows it should create `SongData` objects when creating an `AlbumData` object from an array. - -There are quite a few ways to type data collections: +In the next chapters of this documentation we'll see that is possible to include or exclude properties from the data objects like this: ```php -// Without namespace - -/** @var SongData[] */ -public DataCollection $songs; +class SongController +{ + public function index() + { + return SongData::collect(Song::all(), DataCollection::class)->include('artist'); + } +} +``` -// With namespace +This will only work when you're using a `DataCollection`, `PaginatedDataCollection` or `CursorPaginatedCollection`. -/** @var \App\Data\SongData[] */ -public DataCollection $songs; -// As an array +### DataCollections -/** @var array */ -public DataCollection $songs; +DataCollections provide some extra functionalities like: -// As a data collection +```php +// Counting the amount of items in the collection +count($collection); -/** @var \Spatie\LaravelData\DataCollection */ -public DataCollection $songs; +// Changing an item in the collection +$collection[0]->title = 'Giving Up on Love'; -// With an attribute +// Adding an item to the collection +$collection[] = SongData::from(['title' => 'Never Knew Love', 'artist' => 'Rick Astley']); -#[DataCollectionOf(SongData::class)] -public DataCollection $songs; +// Removing an item from the collection +unset($collection[0]); ``` -You're free to use one of these annotations/attributes as long as you're using one of them when adding a data collection to a data object. +It is even possible to loop over it with a foreach: -## `DataCollection` methods +```php +foreach ($songs as $song){ + echo $song->title; +} +``` The `DataCollection` class implements a few of the Laravel collection methods: @@ -159,5 +199,30 @@ The `DataCollection` class implements a few of the Laravel collection methods: You can for example get the first item within a collection like this: ```php -SongData::collection(Song::all())->first(); // SongData object +SongData::collect(Song::all(), DataCollection::class)->first(); // SongData object +``` + +### The `collection` method + +In previous versions of the package it was possible to use the `collection` method to create a collection of data objects: + +```php +SongData::collection(Song::all()); // returns a DataCollection of SongData objects +SongData::collection(Song::paginate()); // returns a PaginatedDataCollection of SongData objects +SongData::collection(Song::cursorPaginate()); // returns a CursorPaginatedCollection of SongData objects +``` + +This method was removed with version v4 of the package in favor for the more powerful `collect` method. The `collection` method can still be used by using the `WithDeprecatedCollectionMethod` trait: + +```php +use Spatie\LaravelData\WithDeprecatedCollectionMethod; + +class SongData extends Data +{ + use WithDeprecatedCollectionMethod; + + // ... +} ``` + +Please note that this trait will be removed in the next major version of the package. diff --git a/docs/as-a-data-transfer-object/computed.md b/docs/as-a-data-transfer-object/computed.md index dfb49f6c..2c10f95f 100644 --- a/docs/as-a-data-transfer-object/computed.md +++ b/docs/as-a-data-transfer-object/computed.md @@ -1,6 +1,6 @@ --- title: Computed values -weight: 7 +weight: 8 --- Earlier we saw how default values can be set for a data object, the same approach can be used to set computed values, although slightly different: @@ -26,7 +26,6 @@ You can now do the following: ```php SongData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche']); -SongData::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche']); ``` Again there are a few conditions for this approach: diff --git a/docs/as-a-data-transfer-object/creating-a-data-object.md b/docs/as-a-data-transfer-object/creating-a-data-object.md index ef226167..6bda5425 100644 --- a/docs/as-a-data-transfer-object/creating-a-data-object.md +++ b/docs/as-a-data-transfer-object/creating-a-data-object.md @@ -32,8 +32,9 @@ You can use the `from` method to create a data object from nearly anything. For model like this: ```php -class Song extends Model{ - +class Song extends Model +{ + // Your model code } ``` @@ -43,14 +44,14 @@ You can create a data object from such a model like this: SongData::from(Song::firstOrFail($id)); ``` +The package will find the required properties within the model and use them to construct the data object. + Data can also be created from JSON strings: ```php SongData::from('{"title" : "Never Gonna Give You Up","artist" : "Rick Astley"}'); ``` -The package will find the required properties within the model and use them to construct the data object. - Although the PHP 8.0 constructor properties look great in data objects, it is perfectly valid to use regular properties without a constructor like so: ```php @@ -61,56 +62,6 @@ class SongData extends Data } ``` -## Mapping property names - -Sometimes the property names in the array from which you're creating a data object might be different. You can define another name for a property when it is created from array using attributes: - -```php -class ContractData extends Data -{ - public function __construct( - public string $name, - #[MapInputName('record_company')] - public string $recordCompany, - ) { - } -} -``` - -Creating the data object can now be done as such: - -```php -SongData::from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); -``` - -Changing all property names in a data object to snake_case in the data the object is created from can be done as such: - -```php -#[MapInputName(SnakeCaseMapper::class)] -class ContractData extends Data -{ - public function __construct( - public string $name, - public string $recordCompany, - ) { - } -} -``` - -You can also use the `MapName` attribute when you want to combine input (see [transforming data objects](https://spatie.be/docs/laravel-data/v3/as-a-resource/from-data-to-resource#mapping-property-names)) and output property name mapping: - -```php -#[MapName(SnakeCaseMapper::class)] -class ContractData extends Data -{ - public function __construct( - public string $name, - public string $recordCompany, - ) { - } -} -``` - ## Magical creation It is possible to overwrite or extend the behaviour of the `from` method for specific types. So you can construct a data @@ -237,55 +188,3 @@ SongData::withoutMagicalCreationFrom($song); ## Advanced creation Internally this package is using a pipeline to create a data object from something. This pipeline exists of steps which transform properties into a correct structure and it can be completely customized. You can read more about it [here](/docs/laravel-data/v3/advanced-usage/pipeline). - -## Quickly getting data from Models, Requests, ... - -By adding the `WithData` trait to a Model, Request or any class that can be magically be converted to a data object, -you'll enable support for the `getData` method. This method will automatically generate a data object for the object it -is called upon. - -For example, let's retake a look at the `Song` model we saw earlier. We can add the `WithData` trait as follows: - -```php -class Song extends Model{ - use WithData; - - protected $dataClass = SongData::class; -} -``` - -Now we can quickly get the data object for the model as such: - -```php -Song::firstOrFail($id)->getData(); // A SongData object -``` - -We can do the same with a FormRequest, we don't use a property here to define the data class but use a method instead: - -```php -class SongRequest extends FormRequest -{ - use WithData; - - protected function dataClass(): string - { - return SongData::class; - } -} -``` - -Now within a controller where the request is injected, we can get the data object like this: - -```php -class SongController -{ - public function __invoke(SongRequest $request): SongData - { - $data = $request->getData(); - - $song = Song::create($data); - - return $data; - } -} -``` diff --git a/docs/as-a-data-transfer-object/defaults.md b/docs/as-a-data-transfer-object/defaults.md index b8822a8a..a083d4bc 100644 --- a/docs/as-a-data-transfer-object/defaults.md +++ b/docs/as-a-data-transfer-object/defaults.md @@ -1,6 +1,6 @@ --- title: Default values -weight: 6 +weight: 7 --- There are a few ways to define default values for a data object. Since a data object is just a regular PHP class, you can use the constructor to set default values: diff --git a/docs/as-a-data-transfer-object/mapping-property-names.md b/docs/as-a-data-transfer-object/mapping-property-names.md new file mode 100644 index 00000000..6ba5be7b --- /dev/null +++ b/docs/as-a-data-transfer-object/mapping-property-names.md @@ -0,0 +1,52 @@ +--- +title: Mapping property names +weight: 6 +--- + +Sometimes the property names in the array from which you're creating a data object might be different. You can define another name for a property when it is created from array using attributes: + +```php +class ContractData extends Data +{ + public function __construct( + public string $name, + #[MapInputName('record_company')] + public string $recordCompany, + ) { + } +} +``` + +Creating the data object can now be done as such: + +```php +SongData::from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); +``` + +Changing all property names in a data object to snake_case in the data the object is created from can be done as such: + +```php +#[MapInputName(SnakeCaseMapper::class)] +class ContractData extends Data +{ + public function __construct( + public string $name, + public string $recordCompany, + ) { + } +} +``` + +You can also use the `MapName` attribute when you want to combine input (see [transforming data objects](https://spatie.be/docs/laravel-data/v3/as-a-resource/from-data-to-resource#mapping-property-names)) and output property name mapping: + +```php +#[MapName(SnakeCaseMapper::class)] +class ContractData extends Data +{ + public function __construct( + public string $name, + public string $recordCompany, + ) { + } +} +``` diff --git a/docs/as-a-data-transfer-object/nesting.md b/docs/as-a-data-transfer-object/nesting.md index 2800ef5c..434d9b4e 100644 --- a/docs/as-a-data-transfer-object/nesting.md +++ b/docs/as-a-data-transfer-object/nesting.md @@ -1,6 +1,6 @@ --- title: Nesting -weight: 3 +weight: 2 --- It is possible to nest multiple data objects: @@ -46,3 +46,122 @@ AlbumData::from([ ]); ``` +## Collections of data objects + +What if you want to nest a collection of data objects within a data object? + +That's perfectly possible but there's a small catch, you should always define what kind of data objects will be stored +within the collection. This is really important later on to create validation rules for data objects or partially +transforming data objects. + +There are a few different ways to define what kind of data objects will be stored within a collection. You could use an +annotation for example which has as advantage that your IDE will have better suggestions when working with the data +object. And as an extra benefit, static analyzers like PHPStan will also be able to detect errors when you're code +is using the wrong types. + +A collection of data objects defined by annotation looks like this: + +```php +/** + * @property \App\Data\SongData[] $songs + */ +class AlbumData extends Data +{ + public function __construct( + public string $title, + public array $songs, + ) { + } +} +``` + +or like this when using properties: + +```php +class AlbumData extends Data +{ + public string $title; + + /** @var \App\Data\SongData[] */ + public array $songs; +} +``` + +If you've imported the data class you can use the short notation: + +```php +use App\Data\SongData; + +class AlbumData extends Data +{ + /** @var SongData[] */ + public array $songs; +} +``` + +It is also possible to use generics: + +```php +use App\Data\SongData; + +class AlbumData extends Data +{ + /** @var array */ + public array $songs; +} +``` + +The same is true for Laravel collections, but be sure to use two generic parameters to describe the collection. One for the collection key type and one for the data object type. + +```php +use App\Data\SongData; +use \Illuminate\Support\Collection; + +class AlbumData extends Data +{ + /** @var Collection */ + public Collection $songs; +} +``` + +You can also use an attribute to define the type of data objects that will be stored within a collection: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $title, + #[DataCollectionOf(SongData::class)] + public array $songs, + ) { + } +} +``` + +This was the old way to define the type of data objects that will be stored within a collection. It is still supported, but we recommend using the annotation. + +### Creating a data object with collection + +You can create a data object with a collection of data object just like you would create a data object with a nested data object: + +```php +new AlbumData( + 'Never gonna give you up', + [ + new SongData('Never gonna give you up', 'Rick Astley'), + new SongData('Giving up on love', 'Rick Astley'), + ] +); +``` + +Or use the magical creation which will automatically create the data objects for you and also works with collections: + +```php +AlbumData::from([ + 'title' => 'Never Gonna Give You Up', + 'songs' => [ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], + ] +]); +``` diff --git a/docs/as-a-data-transfer-object/optional-properties.md b/docs/as-a-data-transfer-object/optional-properties.md index 4799871a..b37beeea 100644 --- a/docs/as-a-data-transfer-object/optional-properties.md +++ b/docs/as-a-data-transfer-object/optional-properties.md @@ -1,6 +1,6 @@ --- title: Optional properties -weight: 2 +weight: 5 --- Sometimes you have a data object with properties which shouldn't always be set, for example in a partial API update where you only want to update certain fields. In this case you can make a property `Optional` as such: diff --git a/docs/as-a-data-transfer-object/request-to-data-object.md b/docs/as-a-data-transfer-object/request-to-data-object.md index 834119f2..621b3008 100644 --- a/docs/as-a-data-transfer-object/request-to-data-object.md +++ b/docs/as-a-data-transfer-object/request-to-data-object.md @@ -1,6 +1,6 @@ --- title: From a request -weight: 8 +weight: 9 --- You can create a data object by the values given in the request. @@ -32,10 +32,9 @@ You can now inject the `SongData` class in your controller. It will already be f request. ```php -class SongController{ - ... - - public function update( +class UpdateSongController +{ + public function __invoke( Song $model, SongData $data ){ @@ -46,6 +45,26 @@ class SongController{ } ``` +As an added benefit, these values will be validated before the data object is created. If the validation fails, a `ValidationException` will be thrown which will look like you've written the validation rules yourself. + +The package will also automatically validate all requests when passed to the from method: + +```php +class UpdateSongController +{ + public function __invoke( + Song $model, + SongRequest $request + ){ + $model->update(SongData::from($request)->all()); + + return redirect()->back(); + } +} +``` + +We have a complete section within these docs dedicated to validation, you can find it [here](/docs/laravel-data/v3/validation). + ## Using validation When creating a data object from a request, the package can also validate the values from the request that will be used diff --git a/docs/as-a-resource/_index.md b/docs/as-a-resource/_index.md index 8f66c1de..153109e4 100644 --- a/docs/as-a-resource/_index.md +++ b/docs/as-a-resource/_index.md @@ -1,5 +1,5 @@ --- title: As a resource -weight: 3 +weight: 4 --- diff --git a/docs/validation/_index.md b/docs/validation/_index.md new file mode 100644 index 00000000..77dfeedc --- /dev/null +++ b/docs/validation/_index.md @@ -0,0 +1,4 @@ +--- +title: Validation +weight: 3 +--- diff --git a/docs/validation/auto-rule-inferring.md b/docs/validation/auto-rule-inferring.md new file mode 100644 index 00000000..516c5fed --- /dev/null +++ b/docs/validation/auto-rule-inferring.md @@ -0,0 +1,60 @@ +--- +title: Auto rule inferring +weight: 2 +--- + +The package will automatically infer validation rules from the data object. For example, for the following data class: + +```php +class ArtistData extends Data{ + public function __construct( + public string $name, + public int $age, + public ?string $genre, + ) { + } +} +``` + +The package will generate the following validation rules: + +```php +[ + 'name' => ['required', 'string'], + 'age' => ['required', 'integer'], + 'genre' => ['nullable', 'string'], +] +``` + +All these rules are inferred by `RuleInferrers`, special classes that will look at the types of properties and will add rules based upon that. + +Rule inferrers are configured in the `data.php` config file: + +```php +/* + * Rule inferrers can be configured here. They will automatically add + * validation rules to properties of a data object based upon + * the type of the property. + */ +'rule_inferrers' => [ + Spatie\LaravelData\RuleInferrers\SometimesRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\NullableRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer::class, +], +``` + +By default, five rule inferrers are enabled: + +- **SometimesRuleInferrer** will add a `sometimes` rule when the property is optional +- **NullableRuleInferrer** will add a `nullable` rule when the property is nullable +- **RequiredRuleInferrer** will add a `required` rule when the property is not nullable +- **BuiltInTypesRuleInferrer** will add a rules which are based upon the built-in php types: + - An `int` or `float` type will add the `numeric` rule + - A `bool` type will add the `boolean` rule + - A `string` type will add the `string` rule + - A `array` type will add the `array` rule +- **AttributesRuleInferrer** will make sure that rule attributes we described above will also add their rules + +It is possible to write your rule inferrers. You can find more information [here](/docs/laravel-data/v3/advanced-usage/creating-a-rule-inferrer). diff --git a/docs/validation/introduction.md b/docs/validation/introduction.md new file mode 100644 index 00000000..99aad577 --- /dev/null +++ b/docs/validation/introduction.md @@ -0,0 +1,309 @@ +--- +title: Introduction +weight: 1 +--- + +Laravel data allows you to create data objects from all sorts of data. One of the most common ways to create a data object is from a request and the data from a request cannot always be trusted. + +That's why it is possible to validate the data before creating the data object. You can validate requests but also arrays and other structures. + +The package will try to automatically infer validation rules from the data object, so you don't have to write them yourself. For example, a `?string` property will automatically have the `nullable` and `string` rules. + +### Important notice + +Validation is probably one of the coolest features of this package, but it is also the most complex one. We'll try to make it as straightforward as possible to validate data but in the end the Laravel validator was not written to be used in this way. So there are some limitations and quirks you should be aware of. + +In some cases it might be easier to just create a custom request class with validation rules and then call `toArray` on the request to create a data object than trying to validate the data with this package. + +## When does validation happen? + +Validation will always happen BEFORE a data object is created, once a data object is created it is assumed that the data is valid. + +At the moment there isn't a way to validate data objects, so you should implement this logic yourself. We're looking into ways to make this easier in the future. + +Validation runs automatically occasionally: + +- When injecting a data object somewhere and the data object gets created from the request +- When calling the `from` method on a data object with a request + +In all other occasions validation won't run automatically. You can always validate the data manually by calling the `validate` method on a data object: + +```php +SongData::validate( + ['title' => 'Never gonna give you up'] +); // ValidationException will be thrown because 'artist' is missing +``` + +When you also want to create the object after validation was successful you can use `validateAndCreate`: + +```php +SongData::validateAndCreate( + ['title' => 'Never gonna give you up', 'artist' => 'Rick Astley'] +); // returns a SongData object +``` + +## A quick glance at the validation functionality + +We've got a lot of documentation about validation and we suggest you read it all, but if you want to get a quick glance at the validation functionality, here's a quick overview: + +### Auto rule inferring + +The package will automatically infer validation rules from the data object. For example, for the following data class: + +```php +class ArtistData extends Data{ + public function __construct( + public string $name, + public int $age, + public ?string $genre, + ) { + } +} +``` + +The package will generate the following validation rules: + +```php +[ + 'name' => ['required', 'string'], + 'age' => ['required', 'integer'], + 'genre' => ['nullable', 'string'], +] +``` + +The package follows an algorithm to infer rules from the data object, you can read more about it [here](/docs/laravel-data/v3/validation/auto-rule-inferring). + +### Validation attributes + +It is possible to add extra rules as attributes to properties of a data object: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[Max(20)] + public string $artist, + ) { + } +} +``` + +When you provide an artist with a length of more than 20 characters, the validation will fail. + +There's a complete [chapter](/docs/laravel-data/v3/validation/using-attributes) dedicated to validation attributes. + +### Manual rules + +Sometimes you want to add rules manually, this can be done as such: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => ['required', 'string'], + 'artist' => ['required', 'string'], + ]; + } +} +``` + +You can read more about manual rules in its [dedicated chapter](/docs/laravel-data/v3/validation/manual-rules). + +### Using the container + +You can resolve a data object from the container. + +```php +app(SongData::class); +``` + +We resolve a data object from the container, it's properties will already be filled by the values of the request with matching key names. +If the request contains data that is not compatible with the data object, a validation exception will be thrown. + +### Working with the validator + +We provide a few points where you can hook into the validation process. You can read more about it in the [dedicated chapter](/docs/laravel-data/v3/validation/working-with-the-validator). + +It is for example to: + +- overwrite validation messages & attributes +- overwrite the validator itself +- overwrite the redirect when validation fails +- allow to stop validation after a failure +- overwrite the error bag + +### Authorizing a request + +Just like with Laravel requests, it is possible to authorize an action for certain people only: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function authorize(): bool + { + return Auth::user()->name === 'Ruben'; + } +} +``` + +If the method returns `false`, then an `AuthorizationException` is thrown. + +## Validation of nested data objects + +When a data object is nested inside another data object, the validation rules will also be generated for that nested object. + +```php +class SingleData{ + public function __construct( + public ArtistData $artist, + public SongData $song, + ) { + } +} +``` + +The validation rules for this class will be: + +```php +[ + 'artist' => ['array'], + 'artist.name' => ['required', 'string'], + 'artist.age' => ['required', 'integer'], + 'artist.genre' => ['nullable', 'string'], + 'song' => ['array'], + 'song.title' => ['required', 'string'], + 'song.artist' => ['required', 'string'], +] +``` + +There are a few quirky things to keep in mind when working with nested data objects, you can read all about it [here](/docs/laravel-data/v3/validation/nesting-data). + +## Validation of nested data collections + +Let's say we want to create a data object like this from a request: + +```php +class AlbumData extends Data +{ + /** + * @param array $songs + */ + public function __construct( + public string $title, + public array $songs, + ) { + } +} +``` + +Since the `SongData` has its own validation rules, the package will automatically apply them when resolving validation +rules for this object. + +In this case the validation rules for `AlbumData` would look like this: + +```php +[ + 'title' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'songs.*.title' => ['required', 'string'], + 'songs.*.artist' => ['required', 'string'], +] +``` + +More info about nested data collections can be found [here](/docs/laravel-data/v3/validation/nesting-data). + +## Default values + +When you've set some default values for a data object, the validation rules will only be generated if something else than the default is provided. + +For example when we have this data object: + +```php +class SongData extends Data +{ + public function __construct( + public string $title = 'Never Gonna Give You Up', + public string $artist = 'Rick Astley', + ) { + } +} +``` + +And we try to validate the following data: + +```php +SongData::validate( + ['title' => 'Giving Up On Love'] +); +``` + +Then the validation rules will be: + +```php +[ + 'title' => ['required', 'string'], +] +``` + +## Mapping property names + +When mapping property names, the validation rules will be generated for the mapped property name: + +```php +class SongData extends Data +{ + public function __construct( + #[MapFrom('song_title')] + public string $title, + ) { + } +} +``` + +The validation rules for this class will be: + +```php +[ + 'song_title' => ['required', 'string'], +] +``` + +There's one small catch, when the validation fails the error message will be for the original property name, not the mapped property name. This is a small quirk we hope to solve as soon as possible. + +## Retrieving validation rules for a data object + +You can retrieve the validation rules a data object will generate as such: + +```php +AlbumData::getValidationRules($payload); +``` + +This will produce the following array with rules: + +```php +[ + 'title' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'songs.*.title' => ['required', 'string'], + 'songs.*.artist' => ['required', 'string'], +] +``` + +### Payload requirement + +We suggest always to provide a payload when generating validation rules. Because such a payload is used to determine which rules will be generated and which can be skipped. diff --git a/docs/validation/manual-rules.md b/docs/validation/manual-rules.md new file mode 100644 index 00000000..83d5b43f --- /dev/null +++ b/docs/validation/manual-rules.md @@ -0,0 +1,193 @@ +--- +title: Manual rules +weight: 4 +--- + +It is also possible to write rules down manually in a dedicated method on the data object. This can come in handy when you want +to construct a custom rule object which isn't possible with attributes: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => ['required', 'string'], + 'artist' => ['required', 'string'], + ]; + } +} +``` + +By overwriting a property's rules within the `rules` method, no other rules will be inferred automatically anymore for that property. + +This means that in the following example, only a `max:20` rule will be added, and not a `string` and `required` rule: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => ['max:20'], + 'artist' => ['max:20'], + ]; + } +} + +// The generated rules will look like this +[ + 'title' => ['max:20'], + 'artist' => ['max:20'], +] +``` + +As a rule of thumb always follow these rules: + +> Always use the array syntax for defining rules and not a single string which spits the rules by | characters. +> This is needed when using regexes those | can be seen as part of the regex + +## Using attributes + +It is even possible to use the validationAttribute objects within the `rules` method: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => [new Required(), new StringType()], + 'artist' => [new Required(), new StringType()], + ]; + } +} +``` + + +You can even add dependencies to be automatically injected: + +```php +use SongSettingsRepository; + +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(SongSettingsRepository $settings): array + { + return [ + 'title' => [new RequiredIf($settings->forUser(auth()->user())->title_required), new StringType()], + 'artist' => [new Required(), new StringType()], + ]; + } +} +``` + +## Using context + +Sometimes a bit more context is required, in such case a `ValidationContext` parameter can be injected as such: +Additionally, if you need to access the data payload, you can use `$payload` parameter: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(ValidationContext $context): array + { + return [ + 'title' => ['required'], + 'artist' => Rule::requiredIf($context->fullPayload['title'] !== 'Never Gonna Give You Up'), + ]; + } +} +``` + +By default, the provided payload is the whole request payload provided to the data object. +If you want to generate rules in nested data objects then a relative payload can be more useful: + +```php +class AlbumData extends Data +{ + /** + * @param array $songs + */ + public function __construct( + public string $title, + public array $songs, + ) { + } +} + +class SongData extends Data +{ + public function __construct( + public string $title, + public ?string $artist, + ) { + } + + public static function rules(ValidationContext $context): array + { + return [ + 'title' => ['required'], + 'artist' => Rule::requiredIf($context->payload['title'] !== 'Never Gonna Give You Up'), + ]; + } +} +``` + +When providing such payload: + +```php +[ + 'title' => 'Best songs ever made', + 'songs' => [ + ['title' => 'Never Gonna Give You Up'], + ['title' => 'Heroes', 'artist' => 'David Bowie'], + ], +]; +``` + +The rules will be: + +```php +[ + 'title' => ['string', 'required'], + 'songs' => ['present', 'array'], + 'songs.*.title' => ['string', 'required'], + 'songs.*.artist' => ['string', 'nullable'], + 'songs.*' => [NestedRules(...)], +] +``` + +It is also possible to retrieve the current path in the data object chain we're generating rules for right now by calling `$context->path`. In the case of our previous example this would be `songs.0` and `songs.1`; + +Make sure the name of the parameter is `$context` in the `rules` method, otherwise no context will be injected. diff --git a/docs/validation/nesting-data.md b/docs/validation/nesting-data.md new file mode 100644 index 00000000..6f5ba152 --- /dev/null +++ b/docs/validation/nesting-data.md @@ -0,0 +1,6 @@ +--- +title: Nesting Data +weight: 6 +--- + +Work in progress diff --git a/docs/validation/skipping-validation.md b/docs/validation/skipping-validation.md new file mode 100644 index 00000000..c8bb52fa --- /dev/null +++ b/docs/validation/skipping-validation.md @@ -0,0 +1,62 @@ +--- +title: Skipping validation +weight: 7 +--- + +Sometimes you don't want properties to be automatically validated, for instance when you're manually overwriting the +rules method like this: + +```php +class SongData extends Data +{ + public function __construct( + public string $name, + ) { + } + + public static function fromRequest(Request $request): static{ + return new self("{$request->input('first_name')} {$request->input('last_name')}") + } + + public static function rules(): array + { + return [ + 'first_name' => ['required', 'string'], + 'last_name' => ['required', 'string'], + ]; + } +} +``` + +When a request is being validated, the rules will look like this: + +```php +[ + 'name' => ['required', 'string'], + 'first_name' => ['required', 'string'], + 'last_name' => ['required', 'string'], +] +``` + +We know we never want to validate the `name` property since it won't be in the request payload, this can be done as +such: + +```php +class SongData extends Data +{ + public function __construct( + #[WithoutValidation] + public string $name, + ) { + } +} +``` + +Now the validation rules will look like this: + +```php +[ + 'first_name' => ['required', 'string'], + 'last_name' => ['required', 'string'], +] +``` diff --git a/docs/validation/using-validation-attributes.md b/docs/validation/using-validation-attributes.md new file mode 100644 index 00000000..5e437227 --- /dev/null +++ b/docs/validation/using-validation-attributes.md @@ -0,0 +1,168 @@ +--- +title: Using validation attributes +weight: 3 +--- + +It is possible to add extra rules as attributes to properties of a data object: + +```php +class SongData extends Data +{ + public function __construct( + #[Uuid()] + public string $uuid, + #[Max(15), IP, StartsWith('192.')] + public string $ip, + ) { + } +} +``` + +These rules will be merged together with the rules that are inferred from the data object. + +So it is not required to add the `required` and `string` rule, these will be added automatically. The rules for the above data object will look like this: + +```php +[ + 'uuid' => ['required', 'string', 'uuid'], + 'ip' => ['required', 'string', 'max:15', 'ip', 'starts_with:192.'], +] +``` + +For each Laravel validation rule we've got a matching validation attribute, you can find a list of them [here](/docs/laravel-data/v3/advanced-usage/using-attributes). + + +## Referencing route parameters + +Sometimes you need a value within your validation attribute which is a route parameter. +Like the example below where the id should be unique ignoring the current id: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[Unique('songs', ignore: new RouteParameterReference('song'))] + public int $id, + ) { + } +} +``` + +If the parameter is a model and another property should be used, then you can do the following: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[Unique('songs', ignore: new RouteParameterReference('song', 'uuid'))] + public string $uuid, + ) { + } +} +``` + +## Referencing other fields + +It is possible to reference other fields in validation attributes: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[RequiredUnless('title', 'Never Gonna Give You Up')] + public string $artist, + ) { + } +} +``` + +These references are always relative to the current data object. So when being nested like this: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $album_name, + public SongData $song, + ) { + } +} +``` + +The generated rules will look like this: + +```php +[ + 'album_name' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'song.title' => ['required', 'string'], + 'song.artist' => ['string', 'required_if:song.title,"Never Gonna Give You Up"'], +] +``` + +If you want to reference fields starting from the root data object you can do the following: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[RequiredUnless(new FieldReference('album', fromRoot: true), 'Whenever You Need Somebody')] + public string $artist, + ) { + } +} +``` + +The rules will now look like this: + +```php +[ + 'album_name' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'song.title' => ['required', 'string'], + 'song.artist' => ['string', 'required_if:album_name,"Whenever You Need Somebody"'], +] +``` + +## Rule attribute + +One special attribute is the `Rule` attribute. With it, you can write rules just like you would when creating a custom +Laravel request: + +```php +// using an array +#[Rule(['required', 'string'])] +public string $property + +// using a string +#[Rule('required|string')] +public string $property + +// using multiple arguments +#[Rule('required', 'string')] +public string $property +``` + +## Creating your validation attribute + +It is possible to create your own validation attribute by extending the `CustomValidationAttribute` class, this class has a `getRules` method that returns the rules that should be applied to the property. + +```php +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] +class CustomRule extends CustomValidationAttribute +{ + /** + * @return array|object|string + */ + public function getRules(ValidationPath $path): array|object|string; + { + return [new CustomRule()]; + } +} +``` + +Quick note: you can only use these rules as an attribute, not as a class rule within the static `rules` method of the data class. diff --git a/docs/validation/working-with-the-validator.md b/docs/validation/working-with-the-validator.md new file mode 100644 index 00000000..4df37981 --- /dev/null +++ b/docs/validation/working-with-the-validator.md @@ -0,0 +1,162 @@ +--- +title: Working with the validator +weight: 5 +--- + +Sometimes a more fine-grained control over the validation is required. In such case you can hook into the validator. + +## Overwriting messages + +It is possible to overwrite the error messages that will be returned when an error fails: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function messages(): array + { + return [ + 'title.required' => 'A title is required', + 'artist.required' => 'An artist is required', + ]; + } +} +``` + +## Overwriting attributes + +In the default Laravel validation rules, you can overwrite the name of the attribute as such: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function attributes(): array + { + return [ + 'title' => 'titel', + 'artist' => 'artiest', + ]; + } +} +``` + +## Overwriting other validation functionality + +Next to overwriting the validator, attributes and messages it is also possible to overwrite the following functionality. + +The redirect when a validation failed: + +```php +class SongData extends Data +{ + // ... + + public static function redirect(): string + { + return action(HomeController::class); + } +} +``` + +Or the route which will be used to redirect after a validation failed: + +```php +class SongData extends Data +{ + // ... + + public static function redirectRoute(): string + { + return 'home'; + } +} +``` + +Whether to stop validating on the first failure: + +```php +class SongData extends Data +{ + // ... + + public static function stopOnFirstFailure(): bool + { + return true; + } +} +``` + +The name of the error bag: + +```php +class SongData extends Data +{ + // ... + + public static function errorBag(): string + { + return 'never_gonna_give_an_error_up'; + } +} +``` + +### Using dependencies in overwritten functionality + +You can also provide dependencies to be injected in the overwritten validator functionality methods like `messages` +, `attributes`, `redirect`, `redirectRoute`, `stopOnFirstFailure`, `errorBag`: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function attributes( + ValidationAttributesLanguageRepository $validationAttributesLanguageRepository + ): array + { + return [ + 'title' => $validationAttributesLanguageRepository->get('title'), + 'artist' => $validationAttributesLanguageRepository->get('artist'), + ]; + } +} +``` + +## Overwriting the validator + +Before validating the values, it is possible to plugin into the validator. This can be done as such: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function withValidator(Validator $validator): void + { + $validator->after(function ($validator) { + $validator->errors()->add('field', 'Something is wrong with this field!'); + }); + } +} +``` + +Please note that this method will only be called on the root data object that is being validated, all the nested data objects and collections `withValidator` methods will not be called. diff --git a/src/Concerns/DeprecatedData.php b/src/Concerns/WithDeprecatedCollectionMethod.php similarity index 97% rename from src/Concerns/DeprecatedData.php rename to src/Concerns/WithDeprecatedCollectionMethod.php index 0ad3a6ac..96ef1576 100644 --- a/src/Concerns/DeprecatedData.php +++ b/src/Concerns/WithDeprecatedCollectionMethod.php @@ -11,7 +11,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; -trait DeprecatedData +trait WithDeprecatedCollectionMethod { /** @deprecated */ public static function collection(Enumerable|array|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator|DataCollection $items): DataCollection|CursorPaginatedDataCollection|PaginatedDataCollection diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index d4292f76..25b92919 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -5,7 +5,7 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; -use Spatie\LaravelData\Concerns\DeprecatedData; +use Spatie\LaravelData\Concerns\WithDeprecatedCollectionMethod; use Spatie\LaravelData\Contracts\DeprecatedData as DeprecatedDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\Data; @@ -15,7 +15,7 @@ use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\SimpleData; - +use Spatie\LaravelData\Tests\Fakes\Collections\CustomCollection; use function Spatie\Snapshots\assertMatchesJsonSnapshot; use function Spatie\Snapshots\assertMatchesSnapshot; @@ -303,7 +303,7 @@ public static function fromSimpleData(SimpleData $simpleData): static it('can return a custom data collection when collecting data', function () { $class = new class ('') extends Data implements DeprecatedDataContract { - use DeprecatedData; + use WithDeprecatedCollectionMethod; protected static string $_collectionClass = CustomDataCollection::class; @@ -322,7 +322,7 @@ public function __construct(public string $string) it('can return a custom paginated data collection when collecting data', function () { $class = new class ('') extends Data implements DeprecatedDataContract { - use DeprecatedData; + use WithDeprecatedCollectionMethod; protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; @@ -409,3 +409,46 @@ function (string $operation, array $arguments, array $expected) { expect($invaded->_dataContext)->toBeNull(); }); + +it('can use a custom collection extended from collection to collect a collection of data objects', function () { + $collection = SimpleData::collect(new CustomCollection([ + ['string' => 'A'], + ['string' => 'B'], + ])); + + expect($collection)->toBeInstanceOf(CustomCollection::class); + expect($collection[0])->toBeInstanceOf(SimpleData::class); + expect($collection[1])->toBeInstanceOf(SimpleData::class); +}); + +it('can magically collect data', function () { + class TestSomeCustomCollection extends Collection + { + } + + $dataClass = new class () extends Data { + public string $string; + + public static function fromString(string $string): self + { + $s = new self(); + + $s->string = $string; + + return $s; + } + + public static function collectArray(array $items): \TestSomeCustomCollection + { + return new \TestSomeCustomCollection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(\TestSomeCustomCollection::class) + ->all()->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); +}); diff --git a/tests/DataTest.php b/tests/DataTest.php index 8c767898..c306a2de 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1426,42 +1426,6 @@ public function __construct( ]); }); -it('can write collection logic in a class', function () { - class TestSomeCustomCollection extends Collection - { - public function nameAll(): string - { - return $this->map(fn ($data) => $data->string)->join(', '); - } - } - - $dataClass = new class () extends Data { - public string $string; - - public static function fromString(string $string): self - { - $s = new self(); - - $s->string = $string; - - return $s; - } - - public static function collectArray(array $items): \TestSomeCustomCollection - { - return new \TestSomeCustomCollection($items); - } - }; - - expect($dataClass::collect(['a', 'b', 'c'])) - ->toBeInstanceOf(\TestSomeCustomCollection::class) - ->all()->toEqual([ - $dataClass::from('a'), - $dataClass::from('b'), - $dataClass::from('c'), - ]); -}); - it('can set a default value for data object', function () { $dataObject = new class ('', '') extends Data { diff --git a/tests/Fakes/Collections/CustomCollection.php b/tests/Fakes/Collections/CustomCollection.php new file mode 100644 index 00000000..4087b71e --- /dev/null +++ b/tests/Fakes/Collections/CustomCollection.php @@ -0,0 +1,10 @@ + SimpleData::from('A')), - dataCollection: Lazy::create(fn () => SimpleData::collect(['B', 'C'], DataCollection::class)), - fakeModel: Lazy::create(fn () => FakeModel::factory()->create([ - 'string' => 'lazy', - ])), - ); - } - - public static function fromString(string $name): self - { - return new self( - id: 1, - simple: SimpleData::from($name), - dataCollection: SimpleData::collect(['B', 'C'], DataCollection::class), - fakeModel: FakeModel::factory()->create([ - 'string' => 'non-lazy', - ]), - ); - } -} diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 0e5a973a..4e30375d 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -1,5 +1,7 @@ LazyData::from('Hello')), Lazy::create(fn () => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), @@ -97,7 +99,7 @@ public function __construct( ]); }); -it('can include specific nested data', function () { +it('can include specific nested data collections', function () { class TestSpecificDefinedIncludeableCollectedAndNestedLazyData extends Data { public function __construct( @@ -114,43 +116,40 @@ public function __construct( $data = new \TestSpecificDefinedIncludeableCollectedAndNestedLazyData($collection); - expect($data->include('songs.name')->toArray()) - ->toMatchArray([ - 'songs' => [ - ['name' => DummyDto::rick()->name], - ['name' => DummyDto::bon()->name], - ], - ]); + expect($data->include('songs.name')->toArray())->toMatchArray([ + 'songs' => [ + ['name' => DummyDto::rick()->name], + ['name' => DummyDto::bon()->name], + ], + ]); - expect($data->include('songs.{name,artist}')->toArray()) - ->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - ], + expect($data->include('songs.{name,artist}')->toArray())->toMatchArray([ + 'songs' => [ + [ + 'name' => DummyDto::rick()->name, + 'artist' => DummyDto::rick()->artist, ], - ]); + [ + 'name' => DummyDto::bon()->name, + 'artist' => DummyDto::bon()->artist, + ], + ], + ]); - expect($data->include('songs.*')->toArray()) - ->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - 'year' => DummyDto::rick()->year, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - 'year' => DummyDto::bon()->year, - ], + expect($data->include('songs.*')->toArray())->toMatchArray([ + 'songs' => [ + [ + 'name' => DummyDto::rick()->name, + 'artist' => DummyDto::rick()->artist, + 'year' => DummyDto::rick()->year, ], - ]); + [ + 'name' => DummyDto::bon()->name, + 'artist' => DummyDto::bon()->artist, + 'year' => DummyDto::bon()->year, + ], + ], + ]); }); it('can have a conditional lazy data', function () { @@ -312,7 +311,7 @@ public static function create(string $name): static 'include' => 'name', ])); - expect($response->getData(true))->toBeArray(['name' => 'Ruben']); + expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); LazyData::$allowedIncludes = null; @@ -339,7 +338,7 @@ public static function create(string $name): static expect($excludedResponse->getData(true))->toBe([]); }); -it('can disabled excluding data dynamically from the request', function () { +it('can disable excluding data dynamically from the request', function () { DefaultLazyData::$allowedExcludes = []; $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ @@ -365,7 +364,7 @@ public static function create(string $name): static expect($response->getData(true))->toBe([]); }); -it('can disabled only data dynamically from the request', function () { +it('can disable only data dynamically from the request', function () { OnlyData::$allowedOnly = []; $response = OnlyData::from([ @@ -401,7 +400,7 @@ public static function create(string $name): static ]); }); -it('can disabled except data dynamically from the request', function () { +it('can disable except data dynamically from the request', function () { ExceptData::$allowedExcept = []; $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ @@ -436,7 +435,7 @@ public static function create(string $name): static it('will not include lazy optional values when transforming', function () { - $data = new class ('Hello World', Lazy::create(fn () => Optional::make())) extends Data { + $data = new class ('Hello World', Lazy::create(fn () => Optional::create())) extends Data { public function __construct( public string $string, public string|Optional|Lazy $lazy_optional_string, @@ -444,7 +443,7 @@ public function __construct( } }; - expect($data->toArray())->toMatchArray([ + expect(($data)->include('lazy_optional_string')->toArray())->toMatchArray([ 'string' => 'Hello World', ]); }); @@ -459,21 +458,6 @@ public function __construct( expect($data->toArray())->toBe([]); }); -it('includes value if not optional data', function () { - $dataClass = new class () extends Data { - public string|Optional $name; - }; - - $data = $dataClass::from([ - 'name' => 'Freek', - ]); - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - ]); -}); - - it('can conditionally include', function () { expect( MultiLazyData::from(DummyDto::rick())->includeWhen('artist', false)->toArray() @@ -933,25 +917,34 @@ public function __construct( }); -it('can fetch lazy union data', function () { - $data = UnionData::from(1); +it('can fetch lazy properties like regular properties within PHP', function () { - expect($data->id)->toBe(1); - expect($data->simple->string)->toBe('A'); - expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); - expect($data->fakeModel->string)->toBe('lazy'); -}); + $dataClass = new class extends Data { + public int $id; -it('can fetch non-lazy union data', function () { - $data = UnionData::from('A'); + public SimpleData|Lazy $simple; - expect($data->id)->toBe(1); + #[DataCollectionOf(SimpleData::class)] + public DataCollection|Lazy $dataCollection; + + public FakeModel|Lazy $fakeModel; + }; + + $data = $dataClass::from([ + 'id' => 42, + 'simple' => Lazy::create(fn () => SimpleData::from('A')), + 'dataCollection' => Lazy::create(fn () => SimpleData::collect(['B', 'C'], DataCollection::class)), + 'fakeModel' => Lazy::create(fn () => FakeModel::factory()->create([ + 'string' => 'lazy', + ])), + ]); + + expect($data->id)->toBe(42); expect($data->simple->string)->toBe('A'); expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); - expect($data->fakeModel->string)->toBe('non-lazy'); + expect($data->fakeModel->string)->toBe('lazy'); }); - it('has array access and will replicate partialtrees (collection)', function () { $collection = MultiData::collect([ new MultiData('first', 'second'), @@ -986,7 +979,7 @@ public function __construct( ]); }); -it('can disabled manually including data in the request (collection)', function () { +it('can disable manually including data in the request (collection)', function () { LazyData::$allowedIncludes = []; $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ @@ -1093,3 +1086,107 @@ public function __construct( [], ]); }); + +it('can work with the different types of lazy data collections', function ( + Data $dataClass, + Closure $itemsClosure +) { + $data = $dataClass::from([ + 'lazyCollection' => $itemsClosure([ + SimpleData::from('A'), + SimpleData::from('B'), + ]), + + 'nestedLazyCollection' => $itemsClosure([ + NestedLazyData::from('C'), + NestedLazyData::from('D'), + ]), + ]); + + expect($data->toArray())->toMatchArray([]); + + expect($data->include('lazyCollection')->toArray())->toMatchArray([ + 'lazyCollection' => [ + ['string' => 'A'], + ['string' => 'B'], + ], + + 'nestedLazyCollection' => [ + [], + [], + ], + ]); + + expect($data->include('lazyCollection', 'nestedLazyCollection.simple')->toArray())->toMatchArray([ + 'lazyCollection' => [ + ['string' => 'A'], + ['string' => 'B'], + ], + + 'nestedLazyCollection' => [ + ['simple' => ['string' => 'C']], + ['simple' => ['string' => 'D']], + ], + ]); +})->with(function () { + yield 'array' => [ + fn () => new class extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|array $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|array $nestedLazyCollection; + }, + fn () => fn (array $items) => $items, + ]; + + yield 'collection' => [ + fn () => new class extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|Collection $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|Collection $nestedLazyCollection; + }, + fn () => fn (array $items) => $items, + ]; + + yield 'paginator' => [ + fn () => new class extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|LengthAwarePaginator $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|LengthAwarePaginator $nestedLazyCollection; + }, + fn () => fn (array $items) => new LengthAwarePaginator($items, count($items), 15), + ]; +})->skip('Impelemnt further'); + +it('partials are always reset when transforming again', function () { + $dataClass = new class(Lazy::create(fn () => NestedLazyData::from('Hello World'))) extends Data { + public function __construct( + public Lazy|NestedLazyData $nested + ) { + } + }; + + dd($dataClass->include('nested')->exclude()->toArray()); + // ['nested' => ['simple' => ['string' => 'Hello World']],] + $dataClass->toArray(); + // ['nested' => ['simple' => ['string' => 'Hello World']],] + + expect($dataClass->include('nested.simple')->toArray())->toBe([ + 'nested' => ['simple' => ['string' => 'Hello World']], + ]); + + expect($dataClass->include('nested')->toArray())->toBe([ + 'nested' => [], + ]); + + expect($dataClass->include()->toArray())->toBeEmpty(); +})->skip('Add a reset partials method'); + +it('can set partials on a nested data object and these will be respected', function () { + +})->skip('Impelemnt'); From c53bf7cb498f13cac3326ebcdc0e6c2018bd1f37 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 22 Sep 2023 12:58:32 +0000 Subject: [PATCH 033/124] Fix styling --- tests/DataCollectionTest.php | 3 ++- tests/Fakes/Collections/CustomCollection.php | 1 - tests/PartialsTest.php | 11 +++++------ 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 25b92919..42f551c3 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -11,11 +11,12 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Tests\Fakes\Collections\CustomCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\SimpleData; -use Spatie\LaravelData\Tests\Fakes\Collections\CustomCollection; + use function Spatie\Snapshots\assertMatchesJsonSnapshot; use function Spatie\Snapshots\assertMatchesSnapshot; diff --git a/tests/Fakes/Collections/CustomCollection.php b/tests/Fakes/Collections/CustomCollection.php index 4087b71e..75543400 100644 --- a/tests/Fakes/Collections/CustomCollection.php +++ b/tests/Fakes/Collections/CustomCollection.php @@ -6,5 +6,4 @@ class CustomCollection extends Collection { - } diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 4e30375d..963aaf0c 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -24,7 +24,6 @@ use Spatie\LaravelData\Tests\Fakes\OnlyData; use Spatie\LaravelData\Tests\Fakes\PartialClassConditionalData; use Spatie\LaravelData\Tests\Fakes\SimpleData; -use Spatie\LaravelData\Tests\Fakes\UnionData; it('can include a lazy property', function () { $data = new LazyData(Lazy::create(fn () => 'test')); @@ -919,7 +918,7 @@ public function __construct( it('can fetch lazy properties like regular properties within PHP', function () { - $dataClass = new class extends Data { + $dataClass = new class () extends Data { public int $id; public SimpleData|Lazy $simple; @@ -1130,7 +1129,7 @@ public function __construct( ]); })->with(function () { yield 'array' => [ - fn () => new class extends Data { + fn () => new class () extends Data { #[DataCollectionOf(SimpleData::class)] public Lazy|array $lazyCollection; @@ -1141,7 +1140,7 @@ public function __construct( ]; yield 'collection' => [ - fn () => new class extends Data { + fn () => new class () extends Data { #[DataCollectionOf(SimpleData::class)] public Lazy|Collection $lazyCollection; @@ -1152,7 +1151,7 @@ public function __construct( ]; yield 'paginator' => [ - fn () => new class extends Data { + fn () => new class () extends Data { #[DataCollectionOf(SimpleData::class)] public Lazy|LengthAwarePaginator $lazyCollection; @@ -1164,7 +1163,7 @@ public function __construct( })->skip('Impelemnt further'); it('partials are always reset when transforming again', function () { - $dataClass = new class(Lazy::create(fn () => NestedLazyData::from('Hello World'))) extends Data { + $dataClass = new class (Lazy::create(fn () => NestedLazyData::from('Hello World'))) extends Data { public function __construct( public Lazy|NestedLazyData $nested ) { From c02fbfa82244419d0d441be1bf7bb52e8ba81599 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 13 Oct 2023 11:19:10 +0200 Subject: [PATCH 034/124] Small fixes after merge --- docs/advanced-usage/get-data-from-a-class-quickly.md | 2 +- src/Resolvers/TransformedDataResolver.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/get-data-from-a-class-quickly.md b/docs/advanced-usage/get-data-from-a-class-quickly.md index 2887c2ec..094663ef 100644 --- a/docs/advanced-usage/get-data-from-a-class-quickly.md +++ b/docs/advanced-usage/get-data-from-a-class-quickly.md @@ -46,7 +46,7 @@ class SongController { $data = $request->getData(); - $song = Song::create($data); + $song = Song::create($data->toArray()); return $data; } diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index ce01ffd3..d615e976 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -59,6 +59,10 @@ private function transform(BaseData&TransformableData $data, TransformationConte return $payload; } + if ($property->type->isOptional && ! isset($data->{$name})) { + return $payload; + } + if (! $this->shouldIncludeProperty($name, $data->{$name}, $context)) { return $payload; } From 9a310d9031d21848c7260ffdccf2337652bf8965 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Dec 2023 17:00:39 +0100 Subject: [PATCH 035/124] Merge updates --- src/Resolvers/TransformedDataResolver.php | 2 +- tests/DataCollectionTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index d615e976..0e28f233 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -59,7 +59,7 @@ private function transform(BaseData&TransformableData $data, TransformationConte return $payload; } - if ($property->type->isOptional && ! isset($data->{$name})) { + if ($property->type->isOptional && ! array_key_exists($name, get_object_vars($data))) { return $payload; } diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 4f807860..67d0071a 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -80,7 +80,7 @@ }); test('a collection can be rejected', function () { - $collection = SimpleData::collection(['A', 'B']); + $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->reject(fn (SimpleData $data) => $data->string === 'B')->toArray(); From 75f5c6f543658a76d34f353322fa02cee27c93fe Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 4 Dec 2023 09:20:51 +0100 Subject: [PATCH 036/124] Benchmarking --- tests/DataBenchTest.php | 84 +++++++++++++++++++++++++++++++++ tests/Fakes/ComplicatedData.php | 22 ++++++++- tests/Fakes/NestedData.php | 7 +++ tests/Fakes/SimpleData.php | 7 +++ 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 tests/DataBenchTest.php diff --git a/tests/DataBenchTest.php b/tests/DataBenchTest.php new file mode 100644 index 00000000..b04ab70b --- /dev/null +++ b/tests/DataBenchTest.php @@ -0,0 +1,84 @@ +getDataClass(ComplicatedData::class); + app(DataConfig::class)->getDataClass(SimpleData::class); + app(DataConfig::class)->getDataClass(NestedData::class); + + $collection = Collection::times(1000, fn () => clone $data); + + $dataCollection = ComplicatedData::collect($collection, DataCollection::class); + + bench( + fn () => $dataCollection->toArray(), + fn () => $dataCollection->toCollection()->map(fn (ComplicatedData $data) => $data->toUserDefinedToArray()), + name: 'collection', + times: 10 + ); + + bench( + fn() => $data->toArray(), + fn() => $data->toUserDefinedToArray(), + name: 'single', + ); +}); + +function benchSingle(Closure $closure, $times): float{ + $start = microtime(true); + + for ($i = 0; $i < $times; $i++) { + $closure(); + } + + $end = microtime(true); + + return ($end - $start) / $times; +} + +function bench(Closure $data, Closure $userDefined, string $name, $times = 100): void +{ + $dataBench = benchSingle($data, $times); + $userDefinedBench = benchSingle($userDefined, $times); + + dump("{$name} data - " . number_format($dataBench, 10)); + dump("{$name} user defined - " . number_format($userDefinedBench, 10)); + dump("{$name} data is " . round($dataBench / $userDefinedBench,0) . " times slower than user defined"); + +} + + diff --git a/tests/Fakes/ComplicatedData.php b/tests/Fakes/ComplicatedData.php index 12e6eeb4..e815a03b 100644 --- a/tests/Fakes/ComplicatedData.php +++ b/tests/Fakes/ComplicatedData.php @@ -26,11 +26,31 @@ public function __construct( #[WithCast(DateTimeInterfaceCast::class, format: 'd-m-Y', type: CarbonImmutable::class)] public $explicitCast, public DateTime $defaultCast, - public SimpleData $nestedData, + public ?SimpleData $nestedData, /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[] */ public DataCollection $nestedCollection, #[DataCollectionOf(SimpleData::class)] public array $nestedArray, ) { } + + public function toUserDefinedToArray(): array + { + return [ + 'withoutType' => $this->withoutType, + 'int' => $this->int, + 'bool' => $this->bool, + 'float' => $this->float, + 'string' => $this->string, + 'array' => $this->array, + 'nullable' => $this->nullable, + 'undefinable' => $this->undefinable, + 'mixed' => $this->mixed, + 'explicitCast' => $this->explicitCast, + 'defaultCast' => $this->defaultCast, + 'nestedData' => $this->nestedData?->toUserDefinedToArray(), + 'nestedCollection' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection->toCollection()->all()), + 'nestedArray' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), + ]; + } } diff --git a/tests/Fakes/NestedData.php b/tests/Fakes/NestedData.php index 20beb41d..d4f24fc7 100644 --- a/tests/Fakes/NestedData.php +++ b/tests/Fakes/NestedData.php @@ -10,4 +10,11 @@ public function __construct( public SimpleData $simple ) { } + + public function toUserDefinedToArray(): array + { + return [ + 'simple' => $this->simple->toUserDefinedToArray(), + ]; + } } diff --git a/tests/Fakes/SimpleData.php b/tests/Fakes/SimpleData.php index bfe4b97a..1fb270fe 100644 --- a/tests/Fakes/SimpleData.php +++ b/tests/Fakes/SimpleData.php @@ -15,4 +15,11 @@ public static function fromString(string $string): self { return new self($string); } + + public function toUserDefinedToArray(): array + { + return [ + 'string' => $this->string, + ]; + } } From 1462196e331b1c4f26c7d579eb2ef1d3f6161d2e Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Mon, 4 Dec 2023 08:21:18 +0000 Subject: [PATCH 037/124] Fix styling --- tests/DataBenchTest.php | 11 +++++------ tests/Fakes/ComplicatedData.php | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/DataBenchTest.php b/tests/DataBenchTest.php index b04ab70b..c89ded50 100644 --- a/tests/DataBenchTest.php +++ b/tests/DataBenchTest.php @@ -52,13 +52,14 @@ ); bench( - fn() => $data->toArray(), - fn() => $data->toUserDefinedToArray(), + fn () => $data->toArray(), + fn () => $data->toUserDefinedToArray(), name: 'single', ); }); -function benchSingle(Closure $closure, $times): float{ +function benchSingle(Closure $closure, $times): float +{ $start = microtime(true); for ($i = 0; $i < $times; $i++) { @@ -77,8 +78,6 @@ function bench(Closure $data, Closure $userDefined, string $name, $times = 100): dump("{$name} data - " . number_format($dataBench, 10)); dump("{$name} user defined - " . number_format($userDefinedBench, 10)); - dump("{$name} data is " . round($dataBench / $userDefinedBench,0) . " times slower than user defined"); + dump("{$name} data is " . round($dataBench / $userDefinedBench, 0) . " times slower than user defined"); } - - diff --git a/tests/Fakes/ComplicatedData.php b/tests/Fakes/ComplicatedData.php index e815a03b..569bd8bf 100644 --- a/tests/Fakes/ComplicatedData.php +++ b/tests/Fakes/ComplicatedData.php @@ -49,8 +49,8 @@ public function toUserDefinedToArray(): array 'explicitCast' => $this->explicitCast, 'defaultCast' => $this->defaultCast, 'nestedData' => $this->nestedData?->toUserDefinedToArray(), - 'nestedCollection' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection->toCollection()->all()), - 'nestedArray' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), + 'nestedCollection' => array_map(fn (NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection->toCollection()->all()), + 'nestedArray' => array_map(fn (NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), ]; } } From 1b920f374385c60f508dccf91989b5eb38d7997c Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 7 Dec 2023 16:51:26 +0100 Subject: [PATCH 038/124] Performance updates --- benchmarks/SimpleDataBench.php | 77 ++++++++++++ composer.json | 5 +- src/Concerns/TransformableData.php | 5 +- src/Resolvers/TransformedDataResolver.php | 111 ++++++++++-------- src/Support/DataContainer.php | 57 +++++++++ .../PartialTransformationContext.php | 45 ++++--- src/Support/TreeNodes/DisabledTreeNode.php | 1 + .../DateTimeInterfaceTransformer.php | 9 +- tests/Fakes/ComplicatedData.php | 6 +- .../DateTimeInterfaceTransformerTest.php | 4 +- 10 files changed, 242 insertions(+), 78 deletions(-) create mode 100644 benchmarks/SimpleDataBench.php create mode 100644 src/Support/DataContainer.php diff --git a/benchmarks/SimpleDataBench.php b/benchmarks/SimpleDataBench.php new file mode 100644 index 00000000..ecf671e4 --- /dev/null +++ b/benchmarks/SimpleDataBench.php @@ -0,0 +1,77 @@ +createApplication(); + } + + protected function getPackageProviders($app) + { + return [ + LaravelDataServiceProvider::class, + ]; + } + + public function setup() + { + $this->data = new ComplicatedData( + 42, + 42, + true, + 3.14, + 'Hello World', + [1, 1, 2, 3, 5, 8], + null, + Optional::create(), + 42, + CarbonImmutable::create(1994, 05, 16), + new DateTime('1994-05-16T12:00:00+01:00'), + null, + null, + [] +// new SimpleData('hello'), +// new DataCollection(NestedData::class, [ +// new NestedData(new SimpleData('I')), +// new NestedData(new SimpleData('am')), +// new NestedData(new SimpleData('groot')), +// ]), +// [ +// new NestedData(new SimpleData('I')), +// new NestedData(new SimpleData('am')), +// new NestedData(new SimpleData('groot')), +// ], + ); + + app(DataConfig::class)->getDataClass(ComplicatedData::class); + app(DataConfig::class)->getDataClass(SimpleData::class); + } + + #[Revs(5000), Iterations(5), BeforeMethods('setup')] + public function benchDataTransformation() + { + $this->data->toArray(); + } + +// #[Revs(500), Iterations(5), BeforeMethods('setup')] +// public function benchDataManualTransformation() +// { +// $this->data->toUserDefinedToArray(); +// } +} diff --git a/composer.json b/composer.json index 7eb218c5..71159f1e 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "spatie/typescript-transformer" : "v3.x-dev#b89615c", "spatie/pest-plugin-snapshots" : "^1.1", "spatie/phpunit-snapshot-assertions" : "^4.2", - "spatie/test-time" : "^1.2" + "spatie/test-time" : "^1.2", + "ext-xdebug" : "*" }, "autoload" : { "psr-4" : { @@ -57,7 +58,7 @@ "test-coverage" : "vendor/bin/pest --coverage-html coverage", "format" : "vendor/bin/php-cs-fixer fix --allow-risky=yes", "benchmark" : "vendor/bin/phpbench run --report=default", - "benchmark-profiled" : "vendor/bin/phpbench xdebug:profile" + "benchmark-profiled" : "vendor/bin/phpbench " }, "config" : { "sort-packages" : true, diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 825b392b..61769454 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -7,6 +7,7 @@ use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast; use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; @@ -26,8 +27,8 @@ public function transform( } $resolver = match (true) { - $this instanceof BaseDataContract => app(TransformedDataResolver::class), - $this instanceof BaseDataCollectableContract => app(TransformedDataCollectionResolver::class), + $this instanceof BaseDataContract => DataContainer::get()->transformedDataResolver(), + $this instanceof BaseDataCollectableContract => DataContainer::get()->transformedDataCollectionResolver(), default => throw new Exception('Cannot transform data object') }; diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 0e28f233..b5decce0 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -12,6 +12,7 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; @@ -49,38 +50,37 @@ public function execute( private function transform(BaseData&TransformableData $data, TransformationContext $context): array { - return $this->dataConfig - ->getDataClass($data::class) - ->properties - ->reduce(function (array $payload, DataProperty $property) use ($data, $context) { - $name = $property->name; - - if ($property->hidden) { - return $payload; - } - - if ($property->type->isOptional && ! array_key_exists($name, get_object_vars($data))) { - return $payload; - } - - if (! $this->shouldIncludeProperty($name, $data->{$name}, $context)) { - return $payload; - } - - $value = $this->resolvePropertyValue( - $property, - $data->{$name}, - $context, - ); - - if ($context->mapPropertyNames && $property->outputMappedName) { - $name = $property->outputMappedName; - } - - $payload[$name] = $value; - - return $payload; - }, []); + $payload = []; + + foreach ($this->dataConfig->getDataClass($data::class)->properties as $property) { + $name = $property->name; + + if ($property->hidden) { + continue; + } + + if ($property->type->isOptional && ! array_key_exists($name, get_object_vars($data))) { + continue; + } + + if (! $this->shouldIncludeProperty($name, $data->{$name}, $context)) { + continue; + } + + $value = $this->resolvePropertyValue( + $property, + $data->{$name}, + $context, + ); + + if ($context->mapPropertyNames && $property->outputMappedName) { + $name = $property->outputMappedName; + } + + $payload[$name] = $value; + } + + return $payload; } protected function shouldIncludeProperty( @@ -175,24 +175,18 @@ protected function resolvePropertyValue( return null; } - $nextContext = $context->next($property->name); - - if (is_array($value) && ($nextContext->partials->only instanceof AllTreeNode || $nextContext->partials->only instanceof PartialTreeNode)) { - return Arr::only($value, $nextContext->partials->only->getFields()); - } - - if (is_array($value) && ($nextContext->partials->except instanceof AllTreeNode || $nextContext->partials->except instanceof PartialTreeNode)) { - return Arr::except($value, $nextContext->partials->except->getFields()); + if ($transformer = $this->resolveTransformerForValue($property, $value, $context)) { + return $transformer->transform($property, $value); } - if ($transformer = $this->resolveTransformerForValue($property, $value, $nextContext)) { - return $transformer->transform($property, $value); + if (is_array($value) && ! $property->type->kind->isDataCollectable()) { + return $this->resolvePotentialPartialArray($value, $context->next($property->name)); } if ( $value instanceof BaseDataCollectable && $value instanceof TransformableData - && $nextContext->transformValues + && $context->transformValues ) { $wrapExecutionType = match ($context->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::Enabled, @@ -200,13 +194,15 @@ protected function resolvePropertyValue( WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled }; - return $value->transform($nextContext->setWrapExecutionType($wrapExecutionType)); + return $value->transform( + $context->next($property->name)->setWrapExecutionType($wrapExecutionType) + ); } if ( $value instanceof BaseData && $value instanceof TransformableData - && $nextContext->transformValues + && $context->transformValues ) { $wrapExecutionType = match ($context->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::TemporarilyDisabled, @@ -214,13 +210,15 @@ protected function resolvePropertyValue( WrapExecutionType::TemporarilyDisabled => WrapExecutionType::TemporarilyDisabled }; - return $value->transform($nextContext->setWrapExecutionType($wrapExecutionType)); + return $value->transform( + $context->next($property->name)->setWrapExecutionType($wrapExecutionType) + ); } if ( $property->type->kind->isDataCollectable() && is_iterable($value) - && $nextContext->transformValues + && $context->transformValues ) { $wrapExecutionType = match ($context->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::Enabled, @@ -228,15 +226,30 @@ protected function resolvePropertyValue( WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled }; - return app(TransformedDataCollectionResolver::class)->execute( + return DataContainer::get()->transformedDataCollectionResolver()->execute( $value, - $nextContext->setWrapExecutionType($wrapExecutionType) + $context->next($property->name)->setWrapExecutionType($wrapExecutionType) ); } return $value; } + protected function resolvePotentialPartialArray( + array $value, + TransformationContext $nextContext, + ) { + if ($nextContext->partials->only instanceof AllTreeNode || $nextContext->partials->only instanceof PartialTreeNode) { + return Arr::only($value, $nextContext->partials->only->getFields()); + } + + if ($nextContext->partials->except instanceof AllTreeNode || $nextContext->partials->except instanceof PartialTreeNode) { + return Arr::except($value, $nextContext->partials->except->getFields()); + } + + return $value; + } + protected function resolveTransformerForValue( DataProperty $property, mixed $value, diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php new file mode 100644 index 00000000..f9d46dca --- /dev/null +++ b/src/Support/DataContainer.php @@ -0,0 +1,57 @@ +transformedDataResolver)) { + $this->transformedDataResolver = app(TransformedDataResolver::class); + } + + return $this->transformedDataResolver; + } + + public function transformedDataCollectionResolver(): TransformedDataCollectionResolver + { + if (! isset($this->transformedDataCollectionResolver)) { + $this->transformedDataCollectionResolver = app(TransformedDataCollectionResolver::class); + } + + return $this->transformedDataCollectionResolver; + } + + public function partialsParser(): PartialsParser + { + if (! isset($this->partialsParser)) { + $this->partialsParser = app(PartialsParser::class); + } + + return $this->partialsParser; + } +} diff --git a/src/Support/Transformation/PartialTransformationContext.php b/src/Support/Transformation/PartialTransformationContext.php index 59bf978f..95264919 100644 --- a/src/Support/Transformation/PartialTransformationContext.php +++ b/src/Support/Transformation/PartialTransformationContext.php @@ -2,11 +2,10 @@ namespace Spatie\LaravelData\Support\Transformation; -use Closure; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; +use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\Partials\PartialsDefinition; -use Spatie\LaravelData\Support\PartialsParser; use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; use Spatie\LaravelData\Support\TreeNodes\TreeNode; @@ -18,30 +17,25 @@ public function __construct( public TreeNode $only = new DisabledTreeNode(), public TreeNode $except = new DisabledTreeNode(), ) { + } public static function create( BaseData|BaseDataCollectable $data, PartialsDefinition $partialsDefinition, ): self { - $filter = fn (bool|null|Closure $condition, string $definition) => match (true) { - is_bool($condition) => $condition, - $condition === null => false, - is_callable($condition) => $condition($data), - }; - return new self( - app(PartialsParser::class)->execute( - collect($partialsDefinition->includes)->filter($filter)->keys()->all() + DataContainer::get()->partialsParser()->execute( + static::filterDefinitions($data, $partialsDefinition->includes), ), - app(PartialsParser::class)->execute( - collect($partialsDefinition->excludes)->filter($filter)->keys()->all() + DataContainer::get()->partialsParser()->execute( + static::filterDefinitions($data, $partialsDefinition->excludes), ), - app(PartialsParser::class)->execute( - collect($partialsDefinition->only)->filter($filter)->keys()->all() + DataContainer::get()->partialsParser()->execute( + static::filterDefinitions($data, $partialsDefinition->only), ), - app(PartialsParser::class)->execute( - collect($partialsDefinition->except)->filter($filter)->keys()->all() + DataContainer::get()->partialsParser()->execute( + static::filterDefinitions($data, $partialsDefinition->except), ), ); } @@ -65,4 +59,23 @@ public function getNested(string $field): self $this->except->getNested($field), ); } + + private static function filterDefinitions( + BaseData|BaseDataCollectable $data, + array $definitions + ): array { + $filtered = []; + + foreach ($definitions as $definition => $condition) { + if ($condition === true) { + $filtered[] = $definition; + } + + if (is_callable($condition) && $condition($data)) { + $filtered[] = $definition; + } + } + + return $filtered; + } } diff --git a/src/Support/TreeNodes/DisabledTreeNode.php b/src/Support/TreeNodes/DisabledTreeNode.php index bd5648d6..8484a86d 100644 --- a/src/Support/TreeNodes/DisabledTreeNode.php +++ b/src/Support/TreeNodes/DisabledTreeNode.php @@ -4,6 +4,7 @@ class DisabledTreeNode implements TreeNode { + public function merge(TreeNode $other): TreeNode { return $other; diff --git a/src/Transformers/DateTimeInterfaceTransformer.php b/src/Transformers/DateTimeInterfaceTransformer.php index 0f8340fe..19ad2745 100644 --- a/src/Transformers/DateTimeInterfaceTransformer.php +++ b/src/Transformers/DateTimeInterfaceTransformer.php @@ -8,21 +8,22 @@ class DateTimeInterfaceTransformer implements Transformer { + protected string $format; + public function __construct( - protected ?string $format = null, + ?string $format = null, protected ?string $setTimeZone = null ) { + [$this->format] = Arr::wrap($format ?? config('data.date_format')); } public function transform(DataProperty $property, mixed $value): string { - [$format] = Arr::wrap($this->format ?? config('data.date_format')); - /** @var \DateTimeInterface $value */ if ($this->setTimeZone) { $value = (clone $value)->setTimezone(new DateTimeZone($this->setTimeZone)); } - return $value->format(ltrim($format, '!')); + return $value->format(ltrim($this->format, '!')); } } diff --git a/tests/Fakes/ComplicatedData.php b/tests/Fakes/ComplicatedData.php index 569bd8bf..fe34f010 100644 --- a/tests/Fakes/ComplicatedData.php +++ b/tests/Fakes/ComplicatedData.php @@ -28,7 +28,7 @@ public function __construct( public DateTime $defaultCast, public ?SimpleData $nestedData, /** @var \Spatie\LaravelData\Tests\Fakes\SimpleData[] */ - public DataCollection $nestedCollection, + public ?DataCollection $nestedCollection, #[DataCollectionOf(SimpleData::class)] public array $nestedArray, ) { @@ -49,8 +49,8 @@ public function toUserDefinedToArray(): array 'explicitCast' => $this->explicitCast, 'defaultCast' => $this->defaultCast, 'nestedData' => $this->nestedData?->toUserDefinedToArray(), - 'nestedCollection' => array_map(fn (NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection->toCollection()->all()), - 'nestedArray' => array_map(fn (NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), + 'nestedCollection' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection?->toCollection()->all() ?? []), + 'nestedArray' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), ]; } } diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index 2e6dd211..d50eacc5 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -136,14 +136,14 @@ }); it('can transform dates with leading !', function () { + config(['data.date_format' => '!Y-m-d']); + $transformer = new DateTimeInterfaceTransformer(); $class = new class () { public Carbon $carbon; }; - config(['data.date_format' => '!Y-m-d']); - expect( $transformer->transform( DataProperty::create(new ReflectionProperty($class, 'carbon')), From a0a163b66d9ab4b0c8f7e54b04b2800114cadddd Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 21 Dec 2023 14:57:04 +0100 Subject: [PATCH 039/124] Fixes for merge --- composer.json | 2 +- src/Support/DataClass.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5b1c8edf..f46bf673 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "phpstan/extension-installer": "^1.1", "phpunit/phpunit": "^9.3", "spatie/invade": "^1.0", - "spatie/typescript-transformer": "v3.x-dev#b89615c", + "spatie/laravel-typescript-transformer": "^2.3", "spatie/pest-plugin-snapshots": "^1.1", "spatie/phpunit-snapshot-assertions": "^4.2", "spatie/test-time": "^1.2" diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index d9c50e24..7b973c3f 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -46,6 +46,7 @@ public function __construct( public readonly bool $wrappable, public readonly Collection $attributes, public readonly array $dataCollectablePropertyAnnotations, + public readonly DataClassNameMapping $outputNameMapping, ) { } From c7a4be110441c79ab069e5460e433d79f1cfbc9b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 21 Dec 2023 15:09:25 +0100 Subject: [PATCH 040/124] Rollback to typescript transformer 2 --- .../DataTypeScriptCollector.php | 22 +++ .../DataTypeScriptTransformer.php | 162 +++++++++++++++++- .../DataUtilitiesClassPropertyProcessor.php | 139 --------------- .../RemoveLazyTypeProcessor.php | 47 +++++ .../RemoveOptionalTypeProcessor.php | 47 +++++ .../DataTypeScriptTransformerTest.php | 115 +++++++------ ...s_for_data_collection_of_attributes__1.txt | 5 - ...s_for_data_collection_of_attributes__1.txt | 5 - ..._covert_a_data_object_to_typescript__1.txt | 14 -- ...ullable_properties_to_optional_ones__1.txt | 4 - ...properties_using_their_mapped_name__1.json | 1 - ..._properties_using_their_mapped_name__1.txt | 3 - ...nated_data_collection_of_attributes__1.txt | 9 - ...s_for_data_collection_of_attributes__1.txt | 9 - ...nated_data_collection_of_attributes__1.txt | 9 - ...convert_a_data_object_to_Typescript__1.txt | 30 ++-- ..._covert_a_data_object_to_typescript__1.txt | 11 -- ...eScript_property_optional_attribute__1.txt | 4 - ..._properties_using_their_mapped_name__1.txt | 8 +- ...nated_data_collection_of_attributes__1.txt | 18 +- ...s_for_data_collection_of_attributes__1.txt | 18 +- ...ted_data_collection_for_attributes___1.txt | 18 +- ...nated_data_collection_of_attributes__1.txt | 9 - 23 files changed, 380 insertions(+), 327 deletions(-) create mode 100644 src/Support/TypeScriptTransformer/DataTypeScriptCollector.php delete mode 100644 src/Support/TypeScriptTransformer/DataUtilitiesClassPropertyProcessor.php create mode 100644 src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php create mode 100644 src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataCollectableTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataCollectionTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_converts_nullable_properties_to_optional_ones__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.json delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt delete mode 100644 tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt delete mode 100644 tests/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt delete mode 100644 tests/__snapshots__/DataTypeScriptTransformerTest__it_it_respects_a_TypeScript_property_optional_attribute__1.txt delete mode 100644 tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptCollector.php b/src/Support/TypeScriptTransformer/DataTypeScriptCollector.php new file mode 100644 index 00000000..364e40d1 --- /dev/null +++ b/src/Support/TypeScriptTransformer/DataTypeScriptCollector.php @@ -0,0 +1,22 @@ +isSubclassOf(BaseData::class)) { + return null; + } + + $transformer = new DataTypeScriptTransformer($this->config); + + return $transformer->transform($class, $class->getShortName()); + } +} diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 21494cce..eb2e8cd2 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -2,21 +2,169 @@ namespace Spatie\LaravelData\Support\TypeScriptTransformer; +use phpDocumentor\Reflection\Fqsen; +use phpDocumentor\Reflection\Type; +use phpDocumentor\Reflection\Types\Array_; +use phpDocumentor\Reflection\Types\Boolean; +use phpDocumentor\Reflection\Types\Integer; +use phpDocumentor\Reflection\Types\Nullable; +use phpDocumentor\Reflection\Types\Object_; +use phpDocumentor\Reflection\Types\String_; use ReflectionClass; +use ReflectionProperty; +use RuntimeException; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\TypeScriptTransformer\Transformers\ClassTransformer; +use Spatie\LaravelData\Enums\DataTypeKind; +use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Lazy\ClosureLazy; +use Spatie\LaravelTypeScriptTransformer\Transformers\DtoTransformer; +use Spatie\TypeScriptTransformer\Attributes\Optional as TypeScriptOptional; +use Spatie\TypeScriptTransformer\Structures\MissingSymbolsCollection; +use Spatie\TypeScriptTransformer\TypeProcessors\DtoCollectionTypeProcessor; +use Spatie\TypeScriptTransformer\TypeProcessors\ReplaceDefaultsTypeProcessor; +use Spatie\TypeScriptTransformer\Types\StructType; -class DataTypeScriptTransformer extends ClassTransformer +class DataTypeScriptTransformer extends DtoTransformer { - protected function shouldTransform(ReflectionClass $reflection): bool + public function canTransform(ReflectionClass $class): bool { - return $reflection->implementsInterface(BaseData::class); + return $class->isSubclassOf(BaseData::class); } - protected function classPropertyProcessors(): array + protected function typeProcessors(): array { - return array_merge(parent::classPropertyProcessors(), [ - new DataUtilitiesClassPropertyProcessor(), + return [ + new ReplaceDefaultsTypeProcessor( + $this->config->getDefaultTypeReplacements() + ), + new RemoveLazyTypeProcessor(), + new RemoveOptionalTypeProcessor(), + new DtoCollectionTypeProcessor(), + ]; + } + + + protected function transformProperties( + ReflectionClass $class, + MissingSymbolsCollection $missingSymbols + ): string { + $dataClass = app(DataConfig::class)->getDataClass($class->getName()); + + $isOptional = $dataClass->attributes->contains( + fn (object $attribute) => $attribute instanceof TypeScriptOptional + ); + + return array_reduce( + $this->resolveProperties($class), + function (string $carry, ReflectionProperty $property) use ($isOptional, $dataClass, $missingSymbols) { + /** @var \Spatie\LaravelData\Support\DataProperty $dataProperty */ + $dataProperty = $dataClass->properties[$property->getName()]; + + $type = $this->resolveTypeForProperty($property, $dataProperty, $missingSymbols); + + if ($type === null) { + return $carry; + } + + $isOptional = $isOptional + || $dataProperty->attributes->contains( + fn (object $attribute) => $attribute instanceof TypeScriptOptional + ) + || ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class) + || $dataProperty->type->isOptional; + + $transformed = $this->typeToTypeScript( + $type, + $missingSymbols, + $property->getDeclaringClass()->getName(), + ); + + $propertyName = $dataProperty->outputMappedName ?? $dataProperty->name; + + if (! preg_match('/^[$_a-zA-Z][$_a-zA-Z0-9]*$/', $propertyName)) { + $propertyName = "'{$propertyName}'"; + } + + return $isOptional + ? "{$carry}{$propertyName}?: {$transformed};" . PHP_EOL + : "{$carry}{$propertyName}: {$transformed};" . PHP_EOL; + }, + '' + ); + } + + protected function resolveTypeForProperty( + ReflectionProperty $property, + DataProperty $dataProperty, + MissingSymbolsCollection $missingSymbols, + ): ?Type { + if (! $dataProperty->type->kind->isDataCollectable()) { + return $this->reflectionToType( + $property, + $missingSymbols, + ...$this->typeProcessors() + ); + } + + $collectionType = match ($dataProperty->type->kind) { + DataTypeKind::DataCollection, DataTypeKind::Array, DataTypeKind::Enumerable => $this->defaultCollectionType($dataProperty->type->dataClass), + DataTypeKind::Paginator, DataTypeKind::DataPaginatedCollection => $this->paginatedCollectionType($dataProperty->type->dataClass), + DataTypeKind::CursorPaginator, DataTypeKind::DataCursorPaginatedCollection => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), + null => throw new RuntimeException('Cannot end up here since the type is dataCollectable') + }; + + if ($dataProperty->type->isNullable()) { + return new Nullable($collectionType); + } + + return $collectionType; + } + + protected function defaultCollectionType(string $class): Type + { + return new Array_(new Object_(new Fqsen("\\{$class}"))); + } + + protected function paginatedCollectionType(string $class): Type + { + return new StructType([ + 'data' => $this->defaultCollectionType($class), + 'links' => new Array_(new StructType([ + 'url' => new Nullable(new String_()), + 'label' => new String_(), + 'active' => new Boolean(), + ])), + 'meta' => new StructType([ + 'current_page' => new Integer(), + 'first_page_url' => new String_(), + 'from' => new Nullable(new Integer()), + 'last_page' => new Integer(), + 'last_page_url' => new String_(), + 'next_page_url' => new Nullable(new String_()), + 'path' => new String_(), + 'per_page' => new Integer(), + 'prev_page_url' => new Nullable(new String_()), + 'to' => new Nullable(new Integer()), + 'total' => new Integer(), + + ]), + ]); + } + + protected function cursorPaginatedCollectionType(string $class): Type + { + return new StructType([ + 'data' => $this->defaultCollectionType($class), + 'links' => new Array_(), + 'meta' => new StructType([ + 'path' => new String_(), + 'per_page' => new Integer(), + 'next_cursor' => new Nullable(new String_()), + 'next_cursor_url' => new Nullable(new String_()), + 'prev_cursor' => new Nullable(new String_()), + 'prev_cursor_url' => new Nullable(new String_()), + ]), ]); } } diff --git a/src/Support/TypeScriptTransformer/DataUtilitiesClassPropertyProcessor.php b/src/Support/TypeScriptTransformer/DataUtilitiesClassPropertyProcessor.php deleted file mode 100644 index 5b7315e2..00000000 --- a/src/Support/TypeScriptTransformer/DataUtilitiesClassPropertyProcessor.php +++ /dev/null @@ -1,139 +0,0 @@ -getDeclaringClass()); - $dataProperty = $dataClass->properties->get($reflection->getName()); - - if ($dataProperty->hidden) { - return null; - } - - if ($dataProperty->outputMappedName) { - $property->name = new TypeScriptIdentifier($dataProperty->outputMappedName); - } - - if ($dataProperty->type->kind->isDataCollectable()) { - $property->type = $this->replaceCollectableTypeWithArray( - $reflection, - $property->type, - $dataProperty - ); - } - - if (! $property->type instanceof TypeScriptUnion) { - return $property; - } - - for ($i = 0; $i < count($property->type->types); $i++) { - $subType = $property->type->types[$i]; - - if ($subType instanceof TypeReference && $this->shouldHideReference($subType)) { - $property->isOptional = true; - - unset($property->type->types[$i]); - } - } - - $property->type->types = array_values($property->type->types); - - return $property; - } - - protected function replaceCollectableTypeWithArray( - ReflectionProperty $reflection, - TypeScriptNode $node, - DataProperty $dataProperty - ): TypeScriptNode { - if ($node instanceof TypeScriptUnion) { - foreach ($node->types as $i => $subNode) { - $node->types[$i] = $this->replaceCollectableTypeWithArray($reflection, $subNode, $dataProperty); - } - - return $node; - } - - if ( - $node instanceof TypeScriptGeneric - && $node->type instanceof TypeReference - && $this->findReplacementForDataCollectable($node->type) - ) { - $node->type = $this->findReplacementForDataCollectable($node->type); - - return $node; - } - - if ( - $node instanceof TypeReference - && $this->findReplacementForDataCollectable($node) - && $dataProperty->type->dataClass - ) { - return new TypeScriptGeneric( - $this->findReplacementForDataCollectable($node), - [new TypeReference(new ClassStringReference($dataProperty->type->dataClass))] - ); - } - - return $node; - } - - protected function findReplacementForDataCollectable( - TypeReference $reference - ): ?TypeScriptNode { - if (! $reference->reference instanceof ClassStringReference) { - return null; - } - - if ($reference->reference->classString === DataCollection::class) { - return new TypeScriptIdentifier('Array'); - } - - if ($reference->reference->classString === PaginatedDataCollection::class) { - return new TypeReference(new ClassStringReference(LengthAwarePaginator::class)); - } - - if ($reference->reference->classString === CursorPaginatedDataCollection::class) { - return new TypeReference(new ClassStringReference(CursorPaginator::class)); - } - - return null; - } - - protected function shouldHideReference( - TypeReference $reference - ): bool { - if (! $reference->reference instanceof ClassStringReference) { - return false; - } - - return is_a($reference->reference->classString, Lazy::class, true) - || is_a($reference->reference->classString, Optional::class, true); - } -} diff --git a/src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php b/src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php new file mode 100644 index 00000000..ce18e176 --- /dev/null +++ b/src/Support/TypeScriptTransformer/RemoveLazyTypeProcessor.php @@ -0,0 +1,47 @@ +getIterator())) + ->reject(function (Type $type) { + if (! $type instanceof Object_) { + return false; + } + + return is_a((string)$type->getFqsen(), Lazy::class, true); + }); + + if ($types->isEmpty()) { + throw new Exception("Type {$reflection->getDeclaringClass()->name}:{$reflection->getName()} cannot be only Lazy"); + } + + if ($types->count() === 1) { + return $types->first(); + } + + return new Compound($types->all()); + } +} diff --git a/src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php b/src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php new file mode 100644 index 00000000..274281ae --- /dev/null +++ b/src/Support/TypeScriptTransformer/RemoveOptionalTypeProcessor.php @@ -0,0 +1,47 @@ +getIterator())) + ->reject(function (Type $type) { + if (! $type instanceof Object_) { + return false; + } + + return is_a((string)$type->getFqsen(), Optional::class, true); + }); + + if ($types->isEmpty()) { + throw new Exception("Type {$reflection->getDeclaringClass()->name}:{$reflection->getName()} cannot be only Optional"); + } + + if ($types->count() === 1) { + return $types->first(); + } + + return new Compound($types->all()); + } +} diff --git a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php index 8a96de9d..af797b73 100644 --- a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php +++ b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php @@ -19,54 +19,17 @@ use Spatie\Snapshots\Driver; use Spatie\TypeScriptTransformer\Attributes\Optional as TypeScriptOptional; -use Spatie\TypeScriptTransformer\References\Reference; -use Spatie\TypeScriptTransformer\Support\TransformationContext; -use Spatie\TypeScriptTransformer\Support\WritingContext; -use Spatie\TypeScriptTransformer\Transformed\Transformed; -use Spatie\TypeScriptTransformer\Transformed\Untransformable; +use Spatie\TypeScriptTransformer\TypeScriptTransformerConfig; function assertMatchesSnapshot($actual, Driver $driver = null): void { baseAssertMatchesSnapshot(str_replace('\\r\\n', '\\n', $actual), $driver); } -function transformData(Data $data): string -{ - $transformer = app(DataTypeScriptTransformer::class); - - $transformed = $transformer->transform( - new ReflectionClass($data), - new TransformationContext('SomeData', ['App', 'Data']) - ); - - return $transformed->typeScriptNode->write(new WritingContext( - fn (Reference $reference) => '{%'.$reference->humanFriendlyName().'%}' - )); -} - -it('will transform data objects', function () { - $transformer = app(DataTypeScriptTransformer::class); - - $transformed = $transformer->transform( - new ReflectionClass(SimpleData::class), - new TransformationContext('SomeData', ['App', 'Data']) - ); - - expect($transformed)->toBeInstanceOf(Transformed::class); - - $someClass = new class () { - }; - - $transformed = $transformer->transform( - new ReflectionClass($someClass::class), - new TransformationContext('SomeData', ['App', 'Data']) - ); - - expect($transformed)->toBeInstanceOf(Untransformable::class); -}); - it('can convert a data object to Typescript', function () { - $data = new class (null, Optional::create(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), Lazy::closure(fn () => 'Lazy'), SimpleData::from('Simple data'), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, []), new DataCollection(SimpleData::class, [])) extends Data { + $config = TypeScriptTransformerConfig::create(); + + $data = new class (null, Optional::create(), 42, true, 'Hello world', 3.14, ['the', 'meaning', 'of', 'life'], Lazy::create(fn () => 'Lazy'), Lazy::closure(fn () => 'Lazy'), SimpleData::from('Simple data'), SimpleData::collect([], DataCollection::class), SimpleData::collect([], DataCollection::class), SimpleData::collect([], DataCollection::class)) extends Data { public function __construct( public null|int $nullable, public Optional|int $undefineable, @@ -89,11 +52,18 @@ public function __construct( } }; - assertMatchesSnapshot(transformData($data)); + $transformer = new DataTypeScriptTransformer($config); + + $reflection = new ReflectionClass($data); + + expect($transformer->canTransform($reflection))->toBeTrue(); + assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed); }); it('uses the correct types for data collection of attributes', function () { - $collection = new DataCollection(SimpleData::class, []); + $config = TypeScriptTransformerConfig::create(); + + $collection = SimpleData::collect([], DataCollection::class); $data = new class ($collection, $collection, $collection, $collection, $collection, $collection, $collection) extends Data { public function __construct( @@ -115,11 +85,18 @@ public function __construct( } }; - assertMatchesSnapshot(transformData($data)); + $transformer = new DataTypeScriptTransformer($config); + + $reflection = new ReflectionClass($data); + + expect($transformer->canTransform($reflection))->toBeTrue(); + assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed); }); it('uses the correct types for paginated data collection for attributes ', function () { - $collection = new PaginatedDataCollection(SimpleData::class, new LengthAwarePaginator([], 0, 15)); + $config = TypeScriptTransformerConfig::create(); + + $collection = SimpleData::collect(new LengthAwarePaginator([], 0, 15), PaginatedDataCollection::class); $data = new class ($collection, $collection, $collection, $collection, $collection, $collection, $collection) extends Data { public function __construct( @@ -141,11 +118,18 @@ public function __construct( } }; - assertMatchesSnapshot(transformData($data)); + $transformer = new DataTypeScriptTransformer($config); + + $reflection = new ReflectionClass($data); + + expect($transformer->canTransform($reflection))->toBeTrue(); + assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed); }); it('uses the correct types for cursor paginated data collection of attributes', function () { - $collection = new CursorPaginatedDataCollection(SimpleData::class, new CursorPaginator([], 15)); + $config = TypeScriptTransformerConfig::create(); + + $collection = SimpleData::collect(new CursorPaginator([], 15), CursorPaginatedDataCollection::class); $data = new class ($collection, $collection, $collection, $collection, $collection, $collection, $collection) extends Data { public function __construct( @@ -167,10 +151,17 @@ public function __construct( } }; - assertMatchesSnapshot(transformData($data)); + $transformer = new DataTypeScriptTransformer($config); + + $reflection = new ReflectionClass($data); + + expect($transformer->canTransform($reflection))->toBeTrue(); + assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed); }); it('outputs types with properties using their mapped name', function () { + $config = TypeScriptTransformerConfig::create(); + $data = new class ('Good job Ruben', 'Hi Ruben') extends Data { public function __construct( #[MapOutputName(SnakeCaseMapper::class)] @@ -181,10 +172,16 @@ public function __construct( } }; - assertMatchesSnapshot(transformData($data)); + $transformer = new DataTypeScriptTransformer($config); + $reflection = new ReflectionClass($data); + + expect($transformer->canTransform($reflection))->toBeTrue(); + assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed); }); it('it respects a TypeScript property optional attribute', function () { + $config = TypeScriptTransformerConfig::create(); + $data = new class (10, 'Ruben') extends Data { public function __construct( #[TypeScriptOptional] @@ -194,10 +191,24 @@ public function __construct( } }; - assertMatchesSnapshot(transformData($data)); + $transformer = new DataTypeScriptTransformer($config); + $reflection = new ReflectionClass($data); + + $this->assertTrue($transformer->canTransform($reflection)); + $this->assertEquals( + <<transform($reflection, 'DataObject')->transformed + ); }); it('it respects a TypeScript class optional attribute', function () { + $config = TypeScriptTransformerConfig::create(); + #[TypeScriptOptional] class DummyTypeScriptOptionalClass extends Data { @@ -206,7 +217,7 @@ public function __construct( public string $name, ) { } - } + }; $transformer = new DataTypeScriptTransformer($config); $reflection = new ReflectionClass(DummyTypeScriptOptionalClass::class); @@ -221,4 +232,4 @@ public function __construct( TXT, $transformer->transform($reflection, 'DataObject')->transformed ); -})->skip('Should be fixed in TS transformer'); +}); diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataCollectableTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataCollectableTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt deleted file mode 100644 index 1a57ad5c..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataCollectableTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ -dataCollection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -dataCollectionWithNull: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -dataCollectionWithNullable: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataCollectionTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataCollectionTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt deleted file mode 100644 index 1a57ad5c..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataCollectionTypeProcessorTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt +++ /dev/null @@ -1,5 +0,0 @@ -{ -dataCollection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -dataCollectionWithNull: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -dataCollectionWithNullable: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt deleted file mode 100644 index 4bf2d0eb..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt +++ /dev/null @@ -1,14 +0,0 @@ -{ -nullable: number | null; -undefineable?: number; -int: number; -bool: boolean; -string: string; -float: number; -array: Array; -lazy?: string; -simpleData: {%Spatie\LaravelData\Tests\Fakes\SimpleData%}; -dataCollection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -dataCollectionAlternative: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -dataCollectionWithAttribute: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_converts_nullable_properties_to_optional_ones__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_converts_nullable_properties_to_optional_ones__1.txt deleted file mode 100644 index fdb4a630..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_converts_nullable_properties_to_optional_ones__1.txt +++ /dev/null @@ -1,4 +0,0 @@ -{ -id?: number | null; -first_name?: string | null; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.json b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.json deleted file mode 100644 index 19765bd5..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.json +++ /dev/null @@ -1 +0,0 @@ -null diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt deleted file mode 100644 index d6b17f4a..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt +++ /dev/null @@ -1,3 +0,0 @@ -{ -some_camel_case_property: string; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt deleted file mode 100644 index b26c509c..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ -collection: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};}; -collectionWithNull: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; -collectionWithNullable: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; -optionalCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};}; -optionalCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; -lazyCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};}; -lazyCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt deleted file mode 100644 index 2e46c020..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ -collection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -collectionWithNull: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -collectionWithNullable: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -optionalCollection?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -optionalCollectionWithNullable?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -lazyCollection?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -lazyCollectionWithNullable?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; -} \ No newline at end of file diff --git a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt b/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt deleted file mode 100644 index fbeb4758..00000000 --- a/tests/Support/TypeScriptTransformer/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ -collection: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; -collectionWithNull: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -collectionWithNullable: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -optionalCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; -optionalCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -lazyCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; -lazyCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_convert_a_data_object_to_Typescript__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_convert_a_data_object_to_Typescript__1.txt index c441e148..8e3d7003 100644 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_convert_a_data_object_to_Typescript__1.txt +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_convert_a_data_object_to_Typescript__1.txt @@ -1,15 +1,15 @@ -export type SomeData = { -nullable: number | null -undefineable?: number -int: number -bool: boolean -string: string -float: number -array: Array -lazy?: string -closureLazy?: string -simpleData: {%class Spatie\LaravelData\Tests\Fakes\SimpleData%} -dataCollection: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -dataCollectionAlternative: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -dataCollectionWithAttribute: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -}; +{ +nullable: number | null; +undefineable?: number; +int: number; +bool: boolean; +string: string; +float: number; +array: Array; +lazy?: string; +closureLazy: string; +simpleData: {%Spatie\LaravelData\Tests\Fakes\SimpleData%}; +dataCollection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; +dataCollectionAlternative: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; +dataCollectionWithAttribute: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; +} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt deleted file mode 100644 index 4cf1f171..00000000 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_can_covert_a_data_object_to_typescript__1.txt +++ /dev/null @@ -1,11 +0,0 @@ -{ -nullable: number | null; -int: number; -bool: boolean; -string: string; -float: number; -array: Array; -lazy?: string; -simpleData: {%Spatie\LaravelData\Tests\Fakes\SimpleData%}; -dataCollection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; -} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_it_respects_a_TypeScript_property_optional_attribute__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_it_respects_a_TypeScript_property_optional_attribute__1.txt deleted file mode 100644 index 78333aca..00000000 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_it_respects_a_TypeScript_property_optional_attribute__1.txt +++ /dev/null @@ -1,4 +0,0 @@ -export type SomeData = { -id?: number -name: string -}; diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt index 4d0cd745..e5b50a34 100644 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name__1.txt @@ -1,4 +1,4 @@ -export type SomeData = { -some_camel_case_property: string -some:non:standard:property: string -}; +{ +some_camel_case_property: string; +'some:non:standard:property': string; +} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt index 44d5fd37..b26c509c 100644 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_cursor_paginated_data_collection_of_attributes__1.txt @@ -1,9 +1,9 @@ -export type SomeData = { -collection: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -collectionWithNull: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -collectionWithNullable: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -optionalCollection?: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -optionalCollectionWithNullable?: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -lazyCollection?: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -lazyCollectionWithNullable?: {%class Illuminate\Pagination\CursorPaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -}; +{ +collection: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};}; +collectionWithNull: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; +collectionWithNullable: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; +optionalCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};}; +optionalCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; +lazyCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};}; +lazyCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array;meta:{path:string;per_page:number;next_cursor:string | null;next_cursor_url:string | null;prev_cursor:string | null;prev_cursor_url:string | null;};} | null; +} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt index ae9089ca..2e46c020 100644 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_data_collection_of_attributes__1.txt @@ -1,9 +1,9 @@ -export type SomeData = { -collection: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -collectionWithNull: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -collectionWithNullable: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -optionalCollection?: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -optionalCollectionWithNullable?: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -lazyCollection?: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -lazyCollectionWithNullable?: Array<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -}; +{ +collection: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; +collectionWithNull: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; +collectionWithNullable: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; +optionalCollection?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; +optionalCollectionWithNullable?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; +lazyCollection?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>; +lazyCollectionWithNullable?: Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null; +} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_for_attributes___1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_for_attributes___1.txt index 199b0305..fbeb4758 100644 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_for_attributes___1.txt +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_for_attributes___1.txt @@ -1,9 +1,9 @@ -export type SomeData = { -collection: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -collectionWithNull: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -collectionWithNullable: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -optionalCollection?: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -optionalCollectionWithNullable?: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -lazyCollection?: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> -lazyCollectionWithNullable?: {%class Illuminate\Pagination\LengthAwarePaginator%}<{%class Spatie\LaravelData\Tests\Fakes\SimpleData%}> | null -}; +{ +collection: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; +collectionWithNull: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; +collectionWithNullable: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; +optionalCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; +optionalCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; +lazyCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; +lazyCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; +} \ No newline at end of file diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt deleted file mode 100644 index fbeb4758..00000000 --- a/tests/__snapshots__/DataTypeScriptTransformerTest__it_uses_the_correct_types_for_paginated_data_collection_of_attributes__1.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ -collection: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; -collectionWithNull: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -collectionWithNullable: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -optionalCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; -optionalCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -lazyCollection?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};}; -lazyCollectionWithNullable?: {data:Array<{%Spatie\LaravelData\Tests\Fakes\SimpleData%}>;links:Array<{url:string | null;label:string;active:boolean;}>;meta:{current_page:number;first_page_url:string;from:number | null;last_page:number;last_page_url:string;next_page_url:string | null;path:string;per_page:number;prev_page_url:string | null;to:number | null;total:number;};} | null; -} \ No newline at end of file From 2178ea90beddc784ecd50aabe463b81c4acfc8bb Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 21 Dec 2023 14:10:08 +0000 Subject: [PATCH 041/124] Fix styling --- src/Concerns/TransformableData.php | 2 -- src/Support/TreeNodes/DisabledTreeNode.php | 1 - tests/Fakes/ComplicatedData.php | 4 ++-- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 61769454..751cda0e 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -5,8 +5,6 @@ use Exception; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; -use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; -use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast; use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; diff --git a/src/Support/TreeNodes/DisabledTreeNode.php b/src/Support/TreeNodes/DisabledTreeNode.php index 8484a86d..bd5648d6 100644 --- a/src/Support/TreeNodes/DisabledTreeNode.php +++ b/src/Support/TreeNodes/DisabledTreeNode.php @@ -4,7 +4,6 @@ class DisabledTreeNode implements TreeNode { - public function merge(TreeNode $other): TreeNode { return $other; diff --git a/tests/Fakes/ComplicatedData.php b/tests/Fakes/ComplicatedData.php index fe34f010..ebad51e3 100644 --- a/tests/Fakes/ComplicatedData.php +++ b/tests/Fakes/ComplicatedData.php @@ -49,8 +49,8 @@ public function toUserDefinedToArray(): array 'explicitCast' => $this->explicitCast, 'defaultCast' => $this->defaultCast, 'nestedData' => $this->nestedData?->toUserDefinedToArray(), - 'nestedCollection' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection?->toCollection()->all() ?? []), - 'nestedArray' => array_map(fn(NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), + 'nestedCollection' => array_map(fn (NestedData $data) => $data->toUserDefinedToArray(), $this->nestedCollection?->toCollection()->all() ?? []), + 'nestedArray' => array_map(fn (NestedData $data) => $data->toUserDefinedToArray(), $this->nestedArray), ]; } } From 8ccfbb4c09ed0e850ea3d54444e29fb0c39396fb Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 21 Dec 2023 15:15:51 +0100 Subject: [PATCH 042/124] Add better formatting --- lint-staged.config.js | 3 + package-lock.json | 721 +++++++++++++++++++++++++++++++++++++++++ package.json | 11 + 3 files changed, 735 insertions(+) create mode 100644 lint-staged.config.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/lint-staged.config.js b/lint-staged.config.js new file mode 100644 index 00000000..33f947c8 --- /dev/null +++ b/lint-staged.config.js @@ -0,0 +1,3 @@ +module.exports = { + '**/*.php': ['php ./vendor/bin/php-cs-fixer fix --config .php-cs-fixer.dist.php --allow-risky=yes'], +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..720c1a49 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,721 @@ +{ + "name": "laravel-data", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "husky": "^8.0.3", + "lint-staged": "^15.2.0" + } + }, + "node_modules/ansi-escapes": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", + "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "dev": true, + "dependencies": { + "type-fest": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/emoji-regex": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", + "integrity": "sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==", + "dev": true, + "bin": { + "husky": "lib/bin.js" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/lilconfig": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/lint-staged": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.0.tgz", + "integrity": "sha512-TFZzUEV00f+2YLaVPWBWGAMq7So6yQx+GG8YRMDeOEIf95Zn5RyiLMsEiX4KTNl9vq/w+NqRJkLA1kPIo15ufQ==", + "dev": true, + "dependencies": { + "chalk": "5.3.0", + "commander": "11.1.0", + "debug": "4.3.4", + "execa": "8.0.1", + "lilconfig": "3.0.0", + "listr2": "8.0.0", + "micromatch": "4.0.5", + "pidtree": "0.6.0", + "string-argv": "0.3.2", + "yaml": "2.3.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/listr2": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.0.0.tgz", + "integrity": "sha512-u8cusxAcyqAiQ2RhYvV7kRKNLgUvtObIbhOX2NCXqvp1UU32xIg5CT22ykS2TPKJXZWJwtK3IKLiqAGlGNE+Zg==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.0.0", + "rfdc": "^1.3.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/log-update": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.0.0.tgz", + "integrity": "sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==", + "dev": true, + "dependencies": { + "ansi-escapes": "^6.2.0", + "cli-cursor": "^4.0.0", + "slice-ansi": "^7.0.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.0.0.tgz", + "integrity": "sha512-GPQHj7row82Hjo9hKZieKcHIhaAIKOJvFSIZXuCU9OASVZrMNUaZuz++SPVrBjnLsnk4k+z9f2EIypgxf2vNFw==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", + "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "dev": true, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..2aa55e6d --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "devDependencies" : { + "husky" : "^8.0.3", + "lint-staged" : "^15.2.0" + }, + "husky" : { + "hooks" : { + "pre-commit" : "lint-staged" + } + } +} From 07cf351df7ca914c9e487af6b19da8b65e46b136 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 5 Jan 2024 11:48:16 +0100 Subject: [PATCH 043/124] Replace partial trees system --- UPGRADING.md | 2 + src/Concerns/ContextableData.php | 36 ++- src/Concerns/IncludeableData.php | 6 +- src/Concerns/ResponsableData.php | 64 +++- src/Concerns/TransformableData.php | 47 ++- src/Contracts/TransformableData.php | 2 +- src/DataCollection.php | 2 +- .../PartialsTreeFromRequestResolver.php | 69 ---- .../RequestQueryStringPartialsResolver.php | 160 ++++++++++ .../TransformedDataCollectionResolver.php | 10 +- src/Resolvers/TransformedDataResolver.php | 152 +++------ src/Resolvers/VisibleDataFieldsResolver.php | 267 ++++++++++++++++ src/Support/AllowedPartialsParser.php | 51 --- src/Support/DataClass.php | 54 ++-- src/Support/DataContainer.php | 34 +- .../NameMapping/DataClassNameMapping.php | 36 --- .../Partials/ForwardsToPartialsDefinition.php | 43 ++- src/Support/Partials/Partial.php | 127 ++++++++ src/Support/Partials/PartialType.php | 37 +++ src/Support/Partials/ResolvedPartial.php | 128 ++++++++ .../Partials/Segments/AllPartialSegment.php | 11 + .../Segments/FieldsPartialSegment.php | 16 + .../Segments/NestedPartialSegment.php | 15 + .../Partials/Segments/PartialSegment.php | 10 + src/Support/PartialsParser.php | 60 ---- src/Support/Transformation/DataContext.php | 54 +++- .../PartialTransformationContext.php | 81 ----- .../Transformation/TransformationContext.php | 42 ++- .../TransformationContextFactory.php | 145 ++++++++- src/Support/TreeNodes/AllTreeNode.php | 31 -- src/Support/TreeNodes/DisabledTreeNode.php | 31 -- src/Support/TreeNodes/ExcludedTreeNode.php | 31 -- src/Support/TreeNodes/PartialTreeNode.php | 88 ------ src/Support/TreeNodes/TreeNode.php | 16 - .../DataStructuresCacheCommandTest.php | 4 + tests/DataBenchTest.php | 2 +- tests/DataCollectionTest.php | 2 + tests/DataTest.php | 5 - tests/Fakes/DefaultLazyData.php | 12 +- tests/Fakes/ExceptData.php | 12 +- tests/Fakes/LazyData.php | 15 +- tests/Fakes/OnlyData.php | 13 +- tests/PartialsTest.php | 60 ++-- .../PartialsTreeFromRequestResolverTest.php | 295 +++++++++--------- tests/Support/DataClassTest.php | 9 - .../NameMapping/DataClassNameMappingTest.php | 41 --- tests/Support/Partials/PartialTest.php | 74 +++++ tests/Support/PartialsParserTest.php | 268 ---------------- tests/Support/TreeNodes/AllTreeNodeTest.php | 34 -- .../TreeNodes/DisabledTreeNodeTest.php | 32 -- .../TreeNodes/ExcludedTreeNodeTest.php | 33 -- .../Support/TreeNodes/PartialTreeNodeTest.php | 54 ---- 52 files changed, 1517 insertions(+), 1406 deletions(-) delete mode 100644 src/Resolvers/PartialsTreeFromRequestResolver.php create mode 100644 src/Resolvers/RequestQueryStringPartialsResolver.php create mode 100644 src/Resolvers/VisibleDataFieldsResolver.php delete mode 100644 src/Support/AllowedPartialsParser.php delete mode 100644 src/Support/NameMapping/DataClassNameMapping.php create mode 100644 src/Support/Partials/Partial.php create mode 100644 src/Support/Partials/PartialType.php create mode 100644 src/Support/Partials/ResolvedPartial.php create mode 100644 src/Support/Partials/Segments/AllPartialSegment.php create mode 100644 src/Support/Partials/Segments/FieldsPartialSegment.php create mode 100644 src/Support/Partials/Segments/NestedPartialSegment.php create mode 100644 src/Support/Partials/Segments/PartialSegment.php delete mode 100644 src/Support/PartialsParser.php delete mode 100644 src/Support/Transformation/PartialTransformationContext.php delete mode 100644 src/Support/TreeNodes/AllTreeNode.php delete mode 100644 src/Support/TreeNodes/DisabledTreeNode.php delete mode 100644 src/Support/TreeNodes/ExcludedTreeNode.php delete mode 100644 src/Support/TreeNodes/PartialTreeNode.php delete mode 100644 src/Support/TreeNodes/TreeNode.php delete mode 100644 tests/Support/NameMapping/DataClassNameMappingTest.php create mode 100644 tests/Support/Partials/PartialTest.php delete mode 100644 tests/Support/PartialsParserTest.php delete mode 100644 tests/Support/TreeNodes/AllTreeNodeTest.php delete mode 100644 tests/Support/TreeNodes/DisabledTreeNodeTest.php delete mode 100644 tests/Support/TreeNodes/ExcludedTreeNodeTest.php delete mode 100644 tests/Support/TreeNodes/PartialTreeNodeTest.php diff --git a/UPGRADING.md b/UPGRADING.md index efb03726..53f8e87e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -19,11 +19,13 @@ The following things are required when upgrading: - Take a look within the docs what has changed - 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 We advise you to take a look at the following things: - Take a look within your data objects if `DataCollection`'s, `DataPaginatedCollection`'s and `DataCursorPaginatedCollection`'s can be replaced with regular arrays, Laravel Collections and Paginator - Replace `DataCollectionOf` attributes with annotations, providing IDE completion and more info for static analyzers - Replace some `extends Data` definitions with `extends Resource` or `extends Dto` for more minimal data objects +- When using `only` and `except` at the same time on a data object/collection, previously only the except would be executed. From now on, we first execute the except and then the only. ## From v2 to v3 Upgrading to laravel data shouldn't take long, we've documented all possible changes just to provide the whole context. You probably won't have to do anything: diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php index ac6db33b..607d18f1 100644 --- a/src/Concerns/ContextableData.php +++ b/src/Concerns/ContextableData.php @@ -4,10 +4,11 @@ use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; +use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Transformation\DataContext; use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapType; +use SplObjectStorage; trait ContextableData { @@ -21,13 +22,34 @@ public function getDataContext(): DataContext default => new Wrap(WrapType::UseGlobal), }; + $includedPartials = new SplObjectStorage(); + $excludedPartials = new SplObjectStorage(); + $onlyPartials = new SplObjectStorage(); + $exceptPartials = new SplObjectStorage(); + + if ($this instanceof IncludeableDataContract) { + foreach ($this->includeProperties() as $key => $value) { + $includedPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); + } + + foreach ($this->excludeProperties() as $key => $value) { + $excludedPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); + } + + foreach ($this->onlyProperties() as $key => $value) { + $onlyPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); + } + + foreach ($this->exceptProperties() as $key => $value) { + $exceptPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); + } + } + return $this->_dataContext = new DataContext( - new PartialsDefinition( - $this instanceof IncludeableDataContract ? $this->includeProperties() : [], - $this instanceof IncludeableDataContract ? $this->excludeProperties() : [], - $this instanceof IncludeableDataContract ? $this->onlyProperties() : [], - $this instanceof IncludeableDataContract ? $this->exceptProperties() : [], - ), + $includedPartials, + $excludedPartials, + $onlyPartials, + $exceptPartials, $this instanceof WrappableDataContract ? $wrap : new Wrap(WrapType::UseGlobal), ); } diff --git a/src/Concerns/IncludeableData.php b/src/Concerns/IncludeableData.php index 36301b6f..c89490b8 100644 --- a/src/Concerns/IncludeableData.php +++ b/src/Concerns/IncludeableData.php @@ -3,15 +3,17 @@ namespace Spatie\LaravelData\Concerns; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; +use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Partials\PartialsDefinition; +use SplObjectStorage; trait IncludeableData { use ForwardsToPartialsDefinition; - protected function getPartialsDefinition(): PartialsDefinition + protected function getPartialsContainer(): object { - return $this->getDataContext()->partialsDefinition; + return $this->getDataContext(); } protected function includeProperties(): array diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 4643c173..6a07408e 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -5,9 +5,10 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; -use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; -use Spatie\LaravelData\Resolvers\PartialsTreeFromRequestResolver; -use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; +use Spatie\LaravelData\Support\DataContainer; +use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialType; +use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; @@ -20,22 +21,51 @@ trait ResponsableData */ public function toResponse($request) { - $context = TransformationContextFactory::create() - ->wrapExecutionType(WrapExecutionType::Enabled) - ->get($this) - ->mergePartials( - PartialTransformationContext::create( - $this, - $this->getDataContext()->partialsDefinition - ) - ); - - $context = $this instanceof IncludeableDataContract - ? $context->mergePartials(resolve(PartialsTreeFromRequestResolver::class)->execute($this, $request)) - : $context; + $contextFactory = TransformationContextFactory::create() + ->wrapExecutionType(WrapExecutionType::Enabled); + + $includePartials = DataContainer::get()->requestQueryStringPartialsResolver()->execute( + $this, + $request, + PartialType::Include + ); + + if ($includePartials) { + $contextFactory->mergeIncludePartials($includePartials); + } + + $excludePartials = DataContainer::get()->requestQueryStringPartialsResolver()->execute( + $this, + $request, + PartialType::Exclude + ); + + if ($excludePartials) { + $contextFactory->mergeExcludePartials($excludePartials); + } + + $onlyPartials = DataContainer::get()->requestQueryStringPartialsResolver()->execute( + $this, + $request, + PartialType::Only + ); + + if ($onlyPartials) { + $contextFactory->mergeOnlyPartials($onlyPartials); + } + + $exceptPartials = DataContainer::get()->requestQueryStringPartialsResolver()->execute( + $this, + $request, + PartialType::Except + ); + + if ($exceptPartials) { + $contextFactory->mergeExceptPartials($exceptPartials); + } return new JsonResponse( - data: $this->transform($context), + data: $this->transform($contextFactory->get($this)), status: $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK, ); } diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 751cda0e..6dd7fc78 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -7,21 +7,20 @@ use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast; -use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; trait TransformableData { public function transform( - null|TransformationContextFactory|TransformationContext $context = null, + null|TransformationContextFactory|TransformationContext $transformationContext = null, ): array { - if ($context === null) { - $context = new TransformationContext(); + if ($transformationContext === null) { + $transformationContext = new TransformationContext(); } - if ($context instanceof TransformationContextFactory) { - $context = $context->get($this); + if ($transformationContext instanceof TransformationContextFactory) { + $transformationContext = $transformationContext->get($this); } $resolver = match (true) { @@ -30,15 +29,35 @@ public function transform( default => throw new Exception('Cannot transform data object') }; - $localPartials = PartialTransformationContext::create( - $this, - $this->getDataContext()->partialsDefinition - ); + $dataContext = $this->getDataContext(); - return $resolver->execute( - $this, - $context->mergePartials($localPartials) - ); + /** @var TransformationContext $transformationContext */ + + if ($dataContext->includePartials->count() > 0) { + $transformationContext->includedPartials->addAll( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->includePartials) + ); + } + + if ($dataContext->excludePartials->count() > 0) { + $transformationContext->excludedPartials->addAll( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->excludePartials) + ); + } + + if ($dataContext->onlyPartials->count() > 0) { + $transformationContext->onlyPartials->addAll( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->onlyPartials) + ); + } + + if ($dataContext->exceptPartials->count() > 0) { + $transformationContext->exceptPartials->addAll( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->exceptPartials) + ); + } + + return $resolver->execute($this, $transformationContext); } public function all(): array diff --git a/src/Contracts/TransformableData.php b/src/Contracts/TransformableData.php index 55f1095d..41cd2ea4 100644 --- a/src/Contracts/TransformableData.php +++ b/src/Contracts/TransformableData.php @@ -12,7 +12,7 @@ interface TransformableData extends JsonSerializable, Jsonable, Arrayable, EloquentCastable, ContextableData { public function transform( - null|TransformationContextFactory|TransformationContext $context = null, + null|TransformationContextFactory|TransformationContext $transformationContext = null, ): array; public function all(): array; diff --git a/src/DataCollection.php b/src/DataCollection.php index 945bdb0b..d4bf71d0 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -107,7 +107,7 @@ public function offsetGet($offset): mixed $data = $this->items->offsetGet($offset); if ($data instanceof IncludeableDataContract) { - $data->getDataContext()->partialsDefinition->merge($this->getPartialsDefinition()); + $data->getDataContext()->mergePartials($this->getDataContext()); } return $data; diff --git a/src/Resolvers/PartialsTreeFromRequestResolver.php b/src/Resolvers/PartialsTreeFromRequestResolver.php deleted file mode 100644 index 31121f89..00000000 --- a/src/Resolvers/PartialsTreeFromRequestResolver.php +++ /dev/null @@ -1,69 +0,0 @@ -dataConfig->getDataClass(match (true) { - $data instanceof BaseData => $data::class, - $data instanceof BaseDataCollectable => $data->getDataClass(), - default => throw new TypeError('Invalid type of data') - }); - - $requestedIncludesTree = $this->partialsParser->execute( - $request->has('include') ? $this->arrayFromRequest($request, 'include') : [], - $dataClass->outputNameMapping - ); - $requestedExcludesTree = $this->partialsParser->execute( - $request->has('exclude') ? $this->arrayFromRequest($request, 'exclude') : [], - $dataClass->outputNameMapping - ); - $requestedOnlyTree = $this->partialsParser->execute( - $request->has('only') ? $this->arrayFromRequest($request, 'only') : [], - $dataClass->outputNameMapping - ); - $requestedExceptTree = $this->partialsParser->execute( - $request->has('except') ? $this->arrayFromRequest($request, 'except') : [], - $dataClass->outputNameMapping - ); - - $allowedRequestIncludesTree = $this->allowedPartialsParser->execute('allowedRequestIncludes', $dataClass); - $allowedRequestExcludesTree = $this->allowedPartialsParser->execute('allowedRequestExcludes', $dataClass); - $allowedRequestOnlyTree = $this->allowedPartialsParser->execute('allowedRequestOnly', $dataClass); - $allowedRequestExceptTree = $this->allowedPartialsParser->execute('allowedRequestExcept', $dataClass); - - return new PartialTransformationContext( - $requestedIncludesTree->intersect($allowedRequestIncludesTree), - $requestedExcludesTree->intersect($allowedRequestExcludesTree), - $requestedOnlyTree->intersect($allowedRequestOnlyTree), - $requestedExceptTree->intersect($allowedRequestExceptTree) - ); - } - - protected function arrayFromRequest(Request $request, string $key): array - { - $value = $request->get($key); - - return is_array($value) ? $value : explode(',', $value); - } -} diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php new file mode 100644 index 00000000..29b8576a --- /dev/null +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -0,0 +1,160 @@ +getRequestParameterName(); + + if (! $request->has($parameter)) { + return null; + } + + $dataClass = $this->dataConfig->getDataClass(match (true) { + $data instanceof BaseData => $data::class, + $data instanceof BaseDataCollectable => $data->getDataClass(), + default => throw new TypeError('Invalid type of data') + }); + + $partials = new SplObjectStorage(); + + $partialStrings = is_array($request->get($parameter)) + ? $request->get($parameter) + : explode(',', $request->get($parameter)); + + foreach ($partialStrings as $partialString) { + $partial = Partial::create($partialString); + + $partialSegments = $this->validateSegments( + $partial->segments, + $type, + $dataClass + ); + + if ($partialSegments === null) { + continue; + } + + $partials->attach(new Partial($partialSegments, permanent: false, condition: null)); + } + + return $partials; + } + + /** + * @param array $partialSegments + * + * @return array|null + */ + protected function validateSegments( + array $partialSegments, + PartialType $type, + DataClass $dataClass, + ): ?array { + $allowed = $type->getAllowedPartials($dataClass); + + $segment = $partialSegments[0]; + + if ($segment instanceof AllPartialSegment) { + if ($allowed === null || $allowed === ['*']) { + return [$segment]; + } + + return null; + } + + if ($segment instanceof NestedPartialSegment) { + $field = $this->findField($segment->field, $dataClass); + + if ($field === null) { + return null; + } + + $propertyDataClass = $dataClass->properties->get($field)->type->dataClass; + + if ( + $propertyDataClass && + ($allowed === null || $allowed === ['*'] || in_array($field, $allowed)) + ) { + $nextSegments = $this->validateSegments( + array_slice($partialSegments, 1), + $type, + $this->dataConfig->getDataClass($propertyDataClass) + ); + + if ($nextSegments === null) { + return [$segment]; + } + + return [$segment, ...$nextSegments]; + } + + return null; + } + + if ($segment instanceof FieldsPartialSegment) { + $validFields = []; + + $allowsAllFields = $allowed === null || $allowed === ['*']; + + foreach ($segment->fields as $field) { + $field = $this->findField($field, $dataClass); + + if ($field === null) { + continue; + } + + if ($allowsAllFields || in_array($field, $allowed)) { + $validFields[] = $field; + } + } + + if (count($validFields) === 0) { + return null; + } + + return [new FieldsPartialSegment($validFields)]; + } + + return null; + } + + protected function findField( + string $field, + DataClass $dataClass, + ): ?string { + if ($dataClass->properties->has($field)) { + return $field; + } + + if (array_key_exists($field, $dataClass->outputMappedProperties)) { + return $dataClass->outputMappedProperties[$field]; + } + + return null; + } +} diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index 2f1607b6..dd7a2ccc 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -15,6 +15,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Wrapping\Wrap; @@ -40,6 +41,8 @@ public function execute( ? $context->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) : $context; + // TODO: take into account that a DataCollection, PaginatedDataCollection and CursorPaginatedDataCollection also can have partials + if ($items instanceof DataCollection) { return $this->transformItems($items->items(), $wrap, $context, $nestedContext); } @@ -110,12 +113,7 @@ protected function transformationClosure( return $data; } - $localPartials = PartialTransformationContext::create( - $data, - $data->getDataContext()->partialsDefinition - ); - - return app(TransformedDataResolver::class)->execute($data, $context->mergePartials($localPartials)); + return $data->transform($context); }; } } diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index b5decce0..11ef908d 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -10,16 +10,10 @@ use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; -use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Lazy\ConditionalLazy; -use Spatie\LaravelData\Support\Lazy\RelationalLazy; use Spatie\LaravelData\Support\Transformation\TransformationContext; -use Spatie\LaravelData\Support\TreeNodes\AllTreeNode; -use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; -use Spatie\LaravelData\Support\TreeNodes\PartialTreeNode; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Transformers\ArrayableTransformer; use Spatie\LaravelData\Transformers\Transformer; @@ -27,7 +21,8 @@ class TransformedDataResolver { public function __construct( - protected DataConfig $dataConfig + protected DataConfig $dataConfig, + protected VisibleDataFieldsResolver $visibleDataFieldsResolver, ) { } @@ -52,18 +47,14 @@ private function transform(BaseData&TransformableData $data, TransformationConte { $payload = []; - foreach ($this->dataConfig->getDataClass($data::class)->properties as $property) { - $name = $property->name; + $dataClass = $this->dataConfig->getDataClass($data::class); - if ($property->hidden) { - continue; - } + $visibleFields = $this->visibleDataFieldsResolver->execute($data, $dataClass, $context); - if ($property->type->isOptional && ! array_key_exists($name, get_object_vars($data))) { - continue; - } + foreach ($dataClass->properties as $property) { + $name = $property->name; - if (! $this->shouldIncludeProperty($name, $data->{$name}, $context)) { + if (! array_key_exists($name, $visibleFields)) { continue; } @@ -71,6 +62,7 @@ private function transform(BaseData&TransformableData $data, TransformationConte $property, $data->{$name}, $context, + $visibleFields[$name] ?? null, ); if ($context->mapPropertyNames && $property->outputMappedName) { @@ -83,89 +75,11 @@ private function transform(BaseData&TransformableData $data, TransformationConte return $payload; } - protected function shouldIncludeProperty( - string $name, - mixed $value, - TransformationContext $context - ): bool { - if ($value instanceof Optional) { - return false; - } - - if ($this->isPropertyHidden($name, $context)) { - return false; - } - - if (! $value instanceof Lazy) { - return true; - } - - if ($value instanceof RelationalLazy || $value instanceof ConditionalLazy) { - return $value->shouldBeIncluded(); - } - - // Lazy excluded checks - - if ($context->partials->lazyExcluded instanceof AllTreeNode) { - return false; - } - - if ($context->partials->lazyExcluded instanceof PartialTreeNode && $context->partials->lazyExcluded->hasField($name)) { - return false; - } - - // Lazy included checks - - if ($context->partials->lazyIncluded instanceof AllTreeNode) { - return true; - } - - if ($value->isDefaultIncluded()) { - return true; - } - - return $context->partials->lazyIncluded instanceof PartialTreeNode && $context->partials->lazyIncluded->hasField($name); - } - - protected function isPropertyHidden( - string $name, - TransformationContext $context - ): bool { - if ($context->partials->except instanceof AllTreeNode) { - return true; - } - - if ( - $context->partials->except instanceof PartialTreeNode - && $context->partials->except->hasField($name) - && $context->partials->except->getNested($name) instanceof ExcludedTreeNode - ) { - return true; - } - - if ($context->partials->except instanceof PartialTreeNode) { - return false; - } - - if ($context->partials->only instanceof AllTreeNode) { - return false; - } - - if ($context->partials->only instanceof PartialTreeNode && $context->partials->only->hasField($name)) { - return false; - } - - if ($context->partials->only instanceof PartialTreeNode || $context->partials->only instanceof ExcludedTreeNode) { - return true; - } - - return false; - } - protected function resolvePropertyValue( DataProperty $property, mixed $value, - TransformationContext $context, + TransformationContext $currentContext, + ?TransformationContext $fieldContext ): mixed { if ($value instanceof Lazy) { $value = $value->resolve(); @@ -175,52 +89,52 @@ protected function resolvePropertyValue( return null; } - if ($transformer = $this->resolveTransformerForValue($property, $value, $context)) { + if ($transformer = $this->resolveTransformerForValue($property, $value, $currentContext)) { return $transformer->transform($property, $value); } if (is_array($value) && ! $property->type->kind->isDataCollectable()) { - return $this->resolvePotentialPartialArray($value, $context->next($property->name)); + return $this->resolvePotentialPartialArray($value, $fieldContext); } if ( $value instanceof BaseDataCollectable && $value instanceof TransformableData - && $context->transformValues + && $currentContext->transformValues ) { - $wrapExecutionType = match ($context->wrapExecutionType) { + $wrapExecutionType = match ($currentContext->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::Enabled, WrapExecutionType::Disabled => WrapExecutionType::Disabled, WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled }; return $value->transform( - $context->next($property->name)->setWrapExecutionType($wrapExecutionType) + $fieldContext->setWrapExecutionType($wrapExecutionType) ); } if ( $value instanceof BaseData && $value instanceof TransformableData - && $context->transformValues + && $currentContext->transformValues ) { - $wrapExecutionType = match ($context->wrapExecutionType) { + $wrapExecutionType = match ($currentContext->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::TemporarilyDisabled, WrapExecutionType::Disabled => WrapExecutionType::Disabled, WrapExecutionType::TemporarilyDisabled => WrapExecutionType::TemporarilyDisabled }; return $value->transform( - $context->next($property->name)->setWrapExecutionType($wrapExecutionType) + $fieldContext->setWrapExecutionType($wrapExecutionType) ); } if ( $property->type->kind->isDataCollectable() && is_iterable($value) - && $context->transformValues + && $currentContext->transformValues ) { - $wrapExecutionType = match ($context->wrapExecutionType) { + $wrapExecutionType = match ($currentContext->wrapExecutionType) { WrapExecutionType::Enabled => WrapExecutionType::Enabled, WrapExecutionType::Disabled => WrapExecutionType::Disabled, WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled @@ -228,7 +142,7 @@ protected function resolvePropertyValue( return DataContainer::get()->transformedDataCollectionResolver()->execute( $value, - $context->next($property->name)->setWrapExecutionType($wrapExecutionType) + $fieldContext->setWrapExecutionType($wrapExecutionType) ); } @@ -237,14 +151,26 @@ protected function resolvePropertyValue( protected function resolvePotentialPartialArray( array $value, - TransformationContext $nextContext, - ) { - if ($nextContext->partials->only instanceof AllTreeNode || $nextContext->partials->only instanceof PartialTreeNode) { - return Arr::only($value, $nextContext->partials->only->getFields()); + TransformationContext $fieldContext, + ): array { + if ($fieldContext->exceptPartials->count() > 0) { + $partials = []; + + foreach ($fieldContext->exceptPartials as $exceptPartial) { + array_push($partials, ...$exceptPartial->toLaravel()); + } + + return Arr::except($value, $partials); } - if ($nextContext->partials->except instanceof AllTreeNode || $nextContext->partials->except instanceof PartialTreeNode) { - return Arr::except($value, $nextContext->partials->except->getFields()); + if ($fieldContext->onlyPartials->count() > 0) { + $partials = []; + + foreach ($fieldContext->onlyPartials as $onlyPartial) { + array_push($partials, ...$onlyPartial->toLaravel()); + } + + return Arr::only($value, $partials); } return $value; diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php new file mode 100644 index 00000000..fe0c04af --- /dev/null +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -0,0 +1,267 @@ + + */ + public function execute( + BaseData $data, + DataClass $dataClass, + TransformationContext $transformationContext, + ): array { + $dataInitializedFields = get_object_vars($data); + + /** @var array $fields */ + $fields = $dataClass + ->properties + ->reject(function (DataProperty $property) use (&$dataInitializedFields): bool { + if ($property->hidden) { + return true; + } + + if ($property->type->isOptional && ! array_key_exists($property->name, $dataInitializedFields)) { + return true; + } + + return false; + }) + ->map(function (DataProperty $property) use ($transformationContext): null|TransformationContext { + if ( + $property->type->kind->isDataCollectable() + || $property->type->kind->isDataObject() + || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) + ) { + return new TransformationContext( + $transformationContext->transformValues, + $transformationContext->mapPropertyNames, + $transformationContext->wrapExecutionType, + new SplObjectStorage(), + new SplObjectStorage(), + new SplObjectStorage(), + new SplObjectStorage(), + ); + } + + return null; + })->all(); + + $this->performExcept($fields, $transformationContext); + + if (empty($fields)) { + return []; + } + + $this->performOnly($fields, $transformationContext); + + $includedFields = $this->resolveIncludedFields( + $fields, + $transformationContext, + $dataClass, + ); + + $excludedFields = $this->resolveExcludedFields( + $fields, + $transformationContext, + $dataClass, + ); + + foreach ($fields as $field => $fieldTransFormationContext) { + $value = $data->{$field}; + + if ($value instanceof Optional) { + unset($fields[$field]); + + continue; + } + + if (! $value instanceof Lazy) { + continue; + } + + if ($value instanceof RelationalLazy || $value instanceof ConditionalLazy) { + if(! $value->shouldBeIncluded()){ + unset($fields[$field]); + } + + continue; + } + + if (in_array($field, $excludedFields)) { + unset($fields[$field]); + + continue; + } + + if ($value->isDefaultIncluded() || in_array($field, $includedFields)) { + continue; + } + + unset($fields[$field]); + } + + return $fields; + } + + protected function performExcept( + array &$fields, + TransformationContext $transformationContext + ): void { + $exceptFields = []; + + foreach ($transformationContext->exceptPartials as $exceptPartial) { + if ($exceptPartial->isUndefined()) { + continue; + } + + if ($exceptPartial->isAll()) { + $fields = []; + + return; + } + + if ($nested = $exceptPartial->getNested()) { + $fields[$nested]->exceptPartials->attach($exceptPartial->next()); + + continue; + } + + if ($selectedFields = $exceptPartial->getFields()) { + array_push($exceptFields, ...$selectedFields); + } + } + + foreach ($exceptFields as $exceptField) { + unset($fields[$exceptField]); + } + } + + protected function performOnly( + array &$fields, + TransformationContext $transformationContext + ): void { + $onlyFields = null; + + foreach ($transformationContext->onlyPartials as $onlyPartial) { + if ($onlyPartial->isUndefined() || $onlyPartial->isAll()) { + // maybe filtered by next steps + continue; + } + + $onlyFields ??= []; + + if ($nested = $onlyPartial->getNested()) { + $fields[$nested]->onlyPartials->attach($onlyPartial->next()); + $onlyFields[] = $nested; + + continue; + } + + if ($selectedFields = $onlyPartial->getFields()) { + array_push($onlyFields, ...$selectedFields); + } + } + + if ($onlyFields === null) { + return; + } + + foreach ($fields as $fieldName => $fieldContext) { + if (in_array($fieldName, $onlyFields)) { + continue; + } + + unset($fields[$fieldName]); + } + } + + protected function resolveIncludedFields( + array $fields, + TransformationContext $transformationContext, + DataClass $dataClass + ): array { + $includedFields = []; + + + foreach ($transformationContext->includedPartials as $includedPartial) { + if ($includedPartial->isUndefined()) { + continue; + } + + if ($includedPartial->isAll()) { + $includedFields = $dataClass + ->properties + ->filter(fn (DataProperty $property) => $property->type->lazyType !== null) + ->keys() + ->all(); + + break; + } + + if ($nested = $includedPartial->getNested()) { + $fields[$nested]->includedPartials->attach($includedPartial->next()); + $includedFields[] = $nested; + + continue; + } + + if ($selectedFields = $includedPartial->getFields()) { + array_push($includedFields, ...$selectedFields); + } + } + + return $includedFields; + } + + protected function resolveExcludedFields( + array $fields, + TransformationContext $transformationContext, + DataClass $dataClass + ): array { + $excludedFields = []; + + foreach ($transformationContext->excludedPartials as $excludedPartial) { + if ($excludedPartial->isUndefined()) { + continue; + } + + if ($excludedPartial->isAll()) { + $excludedFields = $dataClass + ->properties + ->filter(fn (DataProperty $property) => $property->type->lazyType !== null) + ->keys() + ->all(); + + break; + } + + if ($nested = $excludedPartial->getNested()) { + $fields[$nested]->excludedPartials->attach($excludedPartial->next()); + $excludedFields[] = $nested; + + continue; + } + + if ($selectedFields = $excludedPartial->getFields()) { + array_push($excludedFields, ...$selectedFields); + } + } + + return $excludedFields; + } +} diff --git a/src/Support/AllowedPartialsParser.php b/src/Support/AllowedPartialsParser.php deleted file mode 100644 index 69f708bb..00000000 --- a/src/Support/AllowedPartialsParser.php +++ /dev/null @@ -1,51 +0,0 @@ -name::{$type}(); - - if ($allowed === ['*'] || $allowed === null) { - return new AllTreeNode(); - } - - $nodes = collect($allowed) - ->filter(fn (string $field) => $dataClass->properties->has($field)) - ->mapWithKeys(function (string $field) use ($type, $dataClass) { - /** @var \Spatie\LaravelData\Support\DataProperty $dataProperty */ - $dataProperty = $dataClass->properties->get($field); - - if ($dataProperty->type->kind !== DataTypeKind::Default) { - return [ - $field => $this->execute( - $type, - $this->dataConfig->getDataClass($dataProperty->type->dataClass) - ), - ]; - } - - return [$field => new ExcludedTreeNode()]; - }); - - if ($nodes->isEmpty()) { - return new ExcludedTreeNode(); - } - - return new PartialTreeNode($nodes->all()); - } -} diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 7b973c3f..0316d84e 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData\Support; +use Hoa\Compiler\Llk\TreeNode; use Illuminate\Support\Collection; use ReflectionAttribute; use ReflectionClass; @@ -9,6 +10,7 @@ use ReflectionParameter; use ReflectionProperty; use Spatie\LaravelData\Contracts\AppendableData; +use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\IncludeableData; @@ -17,6 +19,7 @@ use Spatie\LaravelData\Contracts\ValidateableData; use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Mappers\ProvidedNameMapper; +use Spatie\LaravelData\Resolvers\AllowedRequestPartialsResolver; use Spatie\LaravelData\Resolvers\NameMappersResolver; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\NameMapping\DataClassNameMapping; @@ -46,12 +49,19 @@ public function __construct( public readonly bool $wrappable, public readonly Collection $attributes, public readonly array $dataCollectablePropertyAnnotations, - public readonly DataClassNameMapping $outputNameMapping, + public readonly ?array $allowedRequestIncludes, + public readonly ?array $allowedRequestExcludes, + public readonly ?array $allowedRequestOnly, + public readonly ?array $allowedRequestExcept, + public readonly array $outputMappedProperties, ) { } public static function create(ReflectionClass $class): self { + /** @var class-string $name */ + $name = $class->name; + $attributes = static::resolveAttributes($class); $methods = collect($class->getMethods()); @@ -60,7 +70,7 @@ public static function create(ReflectionClass $class): self $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); - if($constructor) { + if ($constructor) { $dataCollectablePropertyAnnotations = array_merge( $dataCollectablePropertyAnnotations, DataCollectableAnnotationReader::create()->getForMethod($constructor) @@ -74,6 +84,14 @@ public static function create(ReflectionClass $class): self $dataCollectablePropertyAnnotations, ); + $responsable = $class->implementsInterface(ResponsableData::class); + + $outputMappedProperties = $properties + ->map(fn (DataProperty $property) => $property->outputMappedName) + ->filter() + ->flip() + ->toArray(); + return new self( name: $class->name, properties: $properties, @@ -83,14 +101,18 @@ public static function create(ReflectionClass $class): self isAbstract: $class->isAbstract(), appendable: $class->implementsInterface(AppendableData::class), includeable: $class->implementsInterface(IncludeableData::class), - responsable: $class->implementsInterface(ResponsableData::class), + responsable: $responsable, transformable: $class->implementsInterface(TransformableData::class), validateable: $class->implementsInterface(ValidateableData::class), defaultable: $class->implementsInterface(DefaultableData::class), wrappable: $class->implementsInterface(WrappableData::class), attributes: $attributes, dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, - outputNameMapping: self::resolveOutputNameMapping($properties), + allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, + allowedRequestExcludes: $responsable ? $name::allowedRequestExcludes() : null, + allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, + allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, + outputMappedProperties: $outputMappedProperties, ); } @@ -164,28 +186,4 @@ protected static function resolveDefaultValues( $values ); } - - protected static function resolveOutputNameMapping( - Collection $properties, - ): DataClassNameMapping { - $mapped = []; - $mappedDataObjects = []; - - $properties->each(function (DataProperty $dataProperty) use (&$mapped, &$mappedDataObjects) { - if ($dataProperty->type->kind->isDataObject() || $dataProperty->type->kind->isDataCollectable()) { - $mappedDataObjects[$dataProperty->name] = $dataProperty->type->dataClass; - } - - if ($dataProperty->outputMappedName === null) { - return; - } - - $mapped[$dataProperty->outputMappedName] = $dataProperty->name; - }); - - return new DataClassNameMapping( - $mapped, - $mappedDataObjects - ); - } } diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index f9d46dca..dff50923 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -2,18 +2,19 @@ namespace Spatie\LaravelData\Support; +use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; class DataContainer { - private static self $instance; + protected static self $instance; - private TransformedDataResolver $transformedDataResolver; + protected ?TransformedDataResolver $transformedDataResolver = null; - private TransformedDataCollectionResolver $transformedDataCollectionResolver; + protected ?TransformedDataCollectionResolver $transformedDataCollectionResolver = null; - private PartialsParser $partialsParser; + protected ?RequestQueryStringPartialsResolver $requestQueryStringPartialsResolver = null; private function __construct() { @@ -30,28 +31,23 @@ public static function get(): DataContainer public function transformedDataResolver(): TransformedDataResolver { - if (! isset($this->transformedDataResolver)) { - $this->transformedDataResolver = app(TransformedDataResolver::class); - } - - return $this->transformedDataResolver; + return $this->transformedDataResolver ??= app(TransformedDataResolver::class); } public function transformedDataCollectionResolver(): TransformedDataCollectionResolver { - if (! isset($this->transformedDataCollectionResolver)) { - $this->transformedDataCollectionResolver = app(TransformedDataCollectionResolver::class); - } - - return $this->transformedDataCollectionResolver; + return $this->transformedDataCollectionResolver ??= app(TransformedDataCollectionResolver::class); } - public function partialsParser(): PartialsParser + public function requestQueryStringPartialsResolver(): RequestQueryStringPartialsResolver { - if (! isset($this->partialsParser)) { - $this->partialsParser = app(PartialsParser::class); - } + return $this->requestQueryStringPartialsResolver ??= app(RequestQueryStringPartialsResolver::class); + } - return $this->partialsParser; + public function reset() + { + $this->transformedDataResolver = null; + $this->transformedDataCollectionResolver = null; + $this->requestQueryStringPartialsResolver = null; } } diff --git a/src/Support/NameMapping/DataClassNameMapping.php b/src/Support/NameMapping/DataClassNameMapping.php deleted file mode 100644 index 152a4fae..00000000 --- a/src/Support/NameMapping/DataClassNameMapping.php +++ /dev/null @@ -1,36 +0,0 @@ - $mapped - * @param array> $mappedDataObjects - */ - public function __construct( - readonly array $mapped, - readonly array $mappedDataObjects, - ) { - } - - public function getOriginal(string $mapped): ?string - { - return $this->mapped[$mapped] ?? null; - } - - public function resolveNextMapping( - DataConfig $dataConfig, - string $mappedOrOriginal - ): ?self { - $dataClass = $this->mappedDataObjects[$mappedOrOriginal] ?? null; - - if ($dataClass === null) { - return null; - } - - return $dataConfig->getDataClass($dataClass)->outputNameMapping; - } -} diff --git a/src/Support/Partials/ForwardsToPartialsDefinition.php b/src/Support/Partials/ForwardsToPartialsDefinition.php index 7c14e26b..cbb4eacd 100644 --- a/src/Support/Partials/ForwardsToPartialsDefinition.php +++ b/src/Support/Partials/ForwardsToPartialsDefinition.php @@ -3,15 +3,24 @@ namespace Spatie\LaravelData\Support\Partials; use Closure; +use SplObjectStorage; trait ForwardsToPartialsDefinition { - abstract protected function getPartialsDefinition(): PartialsDefinition; + /** + * @return object{ + * includePartials: SplObjectStorage, + * excludePartials: SplObjectStorage, + * onlyPartials: SplObjectStorage, + * exceptPartials: SplObjectStorage, + * } + */ + abstract protected function getPartialsContainer(): object; public function include(string ...$includes): static { foreach ($includes as $include) { - $this->getPartialsDefinition()->includes[$include] = true; + $this->getPartialsContainer()->includePartials->attach(Partial::create($include)); } return $this; @@ -20,7 +29,7 @@ public function include(string ...$includes): static public function exclude(string ...$excludes): static { foreach ($excludes as $exclude) { - $this->getPartialsDefinition()->excludes[$exclude] = true; + $this->getPartialsContainer()->excludePartials->attach(Partial::create($exclude)); } return $this; @@ -29,7 +38,7 @@ public function exclude(string ...$excludes): static public function only(string ...$only): static { foreach ($only as $onlyDefinition) { - $this->getPartialsDefinition()->only[$onlyDefinition] = true; + $this->getPartialsContainer()->onlyPartials->attach(Partial::create($onlyDefinition)); } return $this; @@ -38,7 +47,7 @@ public function only(string ...$only): static public function except(string ...$except): static { foreach ($except as $exceptDefinition) { - $this->getPartialsDefinition()->except[$exceptDefinition] = true; + $this->getPartialsContainer()->exceptPartials->attach(Partial::create($exceptDefinition)); } return $this; @@ -46,28 +55,44 @@ public function except(string ...$except): static public function includeWhen(string $include, bool|Closure $condition): static { - $this->getPartialsDefinition()->includes[$include] = $condition; + if (is_callable($condition)) { + $this->getPartialsContainer()->includePartials->attach(Partial::createConditional($include, $condition)); + } else if ($condition === true) { + $this->getPartialsContainer()->includePartials->attach(Partial::create($include)); + } return $this; } public function excludeWhen(string $exclude, bool|Closure $condition): static { - $this->getPartialsDefinition()->excludes[$exclude] = $condition; + if (is_callable($condition)) { + $this->getPartialsContainer()->excludePartials->attach(Partial::createConditional($exclude, $condition)); + } else if ($condition === true) { + $this->getPartialsContainer()->excludePartials->attach(Partial::create($exclude)); + } return $this; } public function onlyWhen(string $only, bool|Closure $condition): static { - $this->getPartialsDefinition()->only[$only] = $condition; + if (is_callable($condition)) { + $this->getPartialsContainer()->onlyPartials->attach(Partial::createConditional($only, $condition)); + } else if ($condition === true) { + $this->getPartialsContainer()->onlyPartials->attach(Partial::create($only)); + } return $this; } public function exceptWhen(string $except, bool|Closure $condition): static { - $this->getPartialsDefinition()->except[$except] = $condition; + if (is_callable($condition)) { + $this->getPartialsContainer()->exceptPartials->attach(Partial::createConditional($except, $condition)); + } else if ($condition === true) { + $this->getPartialsContainer()->exceptPartials->attach(Partial::create($except)); + } return $this; } diff --git a/src/Support/Partials/Partial.php b/src/Support/Partials/Partial.php new file mode 100644 index 00000000..2ebf5f6f --- /dev/null +++ b/src/Support/Partials/Partial.php @@ -0,0 +1,127 @@ + $segments + */ + public function __construct( + public array $segments, + public bool $permanent, + public ?Closure $condition, + ) { + $this->resolvedPartial = new ResolvedPartial($segments); + } + + public static function create( + string $path, + bool $permanent = false + ): self { + return new self( + segments: self::resolveSegmentsFromPath($path), + permanent: $permanent, + condition: null, + ); + } + + public static function createConditional( + string $path, + Closure $condition, + bool $permanent = false + ): self { + return new self( + segments: self::resolveSegmentsFromPath($path), + permanent: $permanent, + condition: $condition, + ); + } + + public static function fromMethodDefinedKeyAndValue( + string|int $key, + string|bool|callable $value, + ) { + if (is_string($value)) { + return self::create($value, permanent: true); + } + + if (is_callable($value)) { + return self::createConditional($key, $value, permanent: true); + } + + return self::create($key, permanent: $value); + } + + protected static function resolveSegmentsFromPath(string $path): array + { + $segmentStrings = explode('.', $path); + $segmentsCount = count($segmentStrings); + + $segments = []; + + foreach ($segmentStrings as $index => $segmentString) { + if ($segmentString === '*') { + $segments[] = new AllPartialSegment(); + + return $segments; + } + + if (str_starts_with($segmentString, '{') && str_ends_with($segmentString, '}')) { + $fields = explode( + ',', + substr($segmentString, 1, -1) + ); + + $segments[] = new FieldsPartialSegment(array_map(fn (string $field) => trim($field), $fields)); + + return $segments; + } + + if ($index !== $segmentsCount - 1) { + $segments[] = new NestedPartialSegment($segmentString); + + continue; + } + + if (empty($segmentString)) { + continue; + } + + $segments[] = new FieldsPartialSegment([$segmentString]); + + return $segments; + } + + return $segments; + } + + public function resolve(BaseData|BaseDataCollectable $data): ?ResolvedPartial + { + if ($this->condition === null) { + return $this->resolvedPartial->reset(); + } + + if (($this->condition)($data)) { + return $this->resolvedPartial->reset(); + } + + return null; + } + + public function __toString(): string + { + return implode('.', $this->segments); + } +} diff --git a/src/Support/Partials/PartialType.php b/src/Support/Partials/PartialType.php new file mode 100644 index 00000000..9fe499a7 --- /dev/null +++ b/src/Support/Partials/PartialType.php @@ -0,0 +1,37 @@ + 'include', + self::Exclude => 'exclude', + self::Only => 'only', + self::Except => 'except', + }; + } + + /** + * @return string[]|null + */ + public function getAllowedPartials(DataClass $dataClass): ?array + { + return match ($this) { + self::Include => $dataClass->allowedRequestIncludes, + self::Exclude => $dataClass->allowedRequestExcludes, + self::Only => $dataClass->allowedRequestOnly, + self::Except => $dataClass->allowedRequestExcept, + }; + } +} diff --git a/src/Support/Partials/ResolvedPartial.php b/src/Support/Partials/ResolvedPartial.php new file mode 100644 index 00000000..5d837861 --- /dev/null +++ b/src/Support/Partials/ResolvedPartial.php @@ -0,0 +1,128 @@ + $segments + * @param int $pointer + */ + public function __construct( + public array $segments, + public int $pointer = 0, + ) { + $this->segmentCount = count($segments); + } + + public function isUndefined() + { + return $this->pointer === $this->segmentCount; + } + + public function isAll() + { + return $this->getCurrentSegment() instanceof AllPartialSegment; + } + + public function getNested(): ?string + { + $segment = $this->getCurrentSegment(); + + if (! $segment instanceof NestedPartialSegment) { + return null; + } + + return $segment->field; + } + + public function getFields(): ?array + { + $segment = $this->getCurrentSegment(); + + if (! $segment instanceof FieldsPartialSegment) { + return null; + } + + return $segment->fields; + } + + /** @return string[] */ + public function toLaravel(): array + { + /** @var array $partials */ + $segments = []; + + for ($i = $this->pointer; $i < $this->segmentCount; $i++) { + $segment = $this->segments[$i]; + + if ($segment instanceof AllPartialSegment) { + $segments[] = '*'; + + continue; + } + + if ($segment instanceof NestedPartialSegment) { + $segments[] = $segment->field; + + continue; + } + + if ($segment instanceof FieldsPartialSegment) { + $segmentsAsString = count($segments) === 0 + ? '' + : implode('.', $segments) . '.'; + + return array_map( + fn (string $field) => "{$segmentsAsString}{$field}", + $segment->fields + ); + } + } + + return [implode('.', $segments)]; + } + + public function next(): self + { + if ($this->isUndefined() || $this->isAll()) { + return $this; + } + + $this->pointer++; + + return $this; + } + + public function back(): self + { + $this->pointer--; + + return $this; + } + + public function reset(): self + { + $this->pointer = 0; + + return $this; + } + + public function getCurrentSegment(): PartialSegment + { + return $this->segments[$this->pointer]; + } + + public function __toString(): string + { + return implode('.', $this->segments) . " (current: {$this->pointer})"; + } +} diff --git a/src/Support/Partials/Segments/AllPartialSegment.php b/src/Support/Partials/Segments/AllPartialSegment.php new file mode 100644 index 00000000..004d4702 --- /dev/null +++ b/src/Support/Partials/Segments/AllPartialSegment.php @@ -0,0 +1,11 @@ +fields).'}'; + } +} diff --git a/src/Support/Partials/Segments/NestedPartialSegment.php b/src/Support/Partials/Segments/NestedPartialSegment.php new file mode 100644 index 00000000..f3ffcdf8 --- /dev/null +++ b/src/Support/Partials/Segments/NestedPartialSegment.php @@ -0,0 +1,15 @@ +field; + } +} diff --git a/src/Support/Partials/Segments/PartialSegment.php b/src/Support/Partials/Segments/PartialSegment.php new file mode 100644 index 00000000..83b4d132 --- /dev/null +++ b/src/Support/Partials/Segments/PartialSegment.php @@ -0,0 +1,10 @@ +values() - ->map(fn (string $child) => $mapping?->getOriginal($child) ?? $child) - ->flip() - ->map(fn () => new ExcludedTreeNode()) - ->all(); - - $nodes = $nodes->merge(new PartialTreeNode($children)); - - continue; - } - - $fieldName = $mapping?->getOriginal($field) ?? $field; - - $nestedNode = $nested === null - ? new ExcludedTreeNode() - : $this->execute([$nested], $mapping?->resolveNextMapping($this->dataConfig, $fieldName)); - - $nodes = $nodes->merge(new PartialTreeNode([ - $fieldName => $nestedNode, - ])); - } - - return $nodes; - } -} diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index 8d06fb3f..e5720cb9 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -2,14 +2,64 @@ namespace Spatie\LaravelData\Support\Transformation; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; +use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\BaseDataCollectable; +use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Wrapping\Wrap; +use SplObjectStorage; class DataContext { + /** + * @param SplObjectStorage $includePartials + * @param SplObjectStorage $excludePartials + * @param SplObjectStorage $onlyPartials + * @param SplObjectStorage $exceptPartials + */ public function __construct( - public PartialsDefinition $partialsDefinition = new PartialsDefinition(), + public SplObjectStorage $includePartials, + public SplObjectStorage $excludePartials, + public SplObjectStorage $onlyPartials, + public SplObjectStorage $exceptPartials, public ?Wrap $wrap = null, ) { } + + public function mergePartials(DataContext $dataContext): self + { + $this->includePartials->addAll($dataContext->includePartials); + $this->excludePartials->addAll($dataContext->excludePartials); + $this->onlyPartials->addAll($dataContext->onlyPartials); + $this->exceptPartials->addAll($dataContext->exceptPartials); + + return $this; + } + + /** + * @param SplObjectStorage $partials + * + * @return SplObjectStorage + */ + public function getResolvedPartialsAndRemoveTemporaryOnes( + BaseData|BaseDataCollectable $data, + SplObjectStorage $partials, + ): SplObjectStorage { + $resolvedPartials = new SplObjectStorage(); + $partialsToDetach = new SplObjectStorage(); + + foreach ($partials as $partial) { + if ($resolved = $partial->resolve($data)) { + $resolvedPartials->attach($resolved); + } + + if (! $partial->permanent) { + $partialsToDetach->attach($partial); + } + } + + $partials->removeAll($partialsToDetach); + + return $resolvedPartials; + } } diff --git a/src/Support/Transformation/PartialTransformationContext.php b/src/Support/Transformation/PartialTransformationContext.php deleted file mode 100644 index 95264919..00000000 --- a/src/Support/Transformation/PartialTransformationContext.php +++ /dev/null @@ -1,81 +0,0 @@ -partialsParser()->execute( - static::filterDefinitions($data, $partialsDefinition->includes), - ), - DataContainer::get()->partialsParser()->execute( - static::filterDefinitions($data, $partialsDefinition->excludes), - ), - DataContainer::get()->partialsParser()->execute( - static::filterDefinitions($data, $partialsDefinition->only), - ), - DataContainer::get()->partialsParser()->execute( - static::filterDefinitions($data, $partialsDefinition->except), - ), - ); - } - - public function merge(self $other): self - { - return new self( - $this->lazyIncluded->merge($other->lazyIncluded), - $this->lazyExcluded->merge($other->lazyExcluded), - $this->only->merge($other->only), - $this->except->merge($other->except), - ); - } - - public function getNested(string $field): self - { - return new self( - $this->lazyIncluded->getNested($field), - $this->lazyExcluded->getNested($field), - $this->only->getNested($field), - $this->except->getNested($field), - ); - } - - private static function filterDefinitions( - BaseData|BaseDataCollectable $data, - array $definitions - ): array { - $filtered = []; - - foreach ($definitions as $definition => $condition) { - if ($condition === true) { - $filtered[] = $definition; - } - - if (is_callable($condition) && $condition($data)) { - $filtered[] = $definition; - } - } - - return $filtered; - } -} diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index b7fa2db4..69542707 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -2,46 +2,42 @@ namespace Spatie\LaravelData\Support\Transformation; +use Exception; +use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsDefinition; +use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; +use SplObjectStorage; class TransformationContext { + /** + * @param SplObjectStorage $includedPartials + * @param SplObjectStorage $excludedPartials + * @param SplObjectStorage $onlyPartials + * @param SplObjectStorage $exceptPartials + */ public function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public PartialTransformationContext $partials = new PartialTransformationContext(), + public SplObjectStorage $includedPartials = new SplObjectStorage(), + public SplObjectStorage $excludedPartials = new SplObjectStorage(), + public SplObjectStorage $onlyPartials = new SplObjectStorage(), + public SplObjectStorage $exceptPartials = new SplObjectStorage(), ) { } - public function next( - string $property, - ): self { - return new self( - $this->transformValues, - $this->mapPropertyNames, - $this->wrapExecutionType, - $this->partials->getNested($property) - ); - } - - public function mergePartials(PartialTransformationContext $partials): self - { - return new self( - $this->transformValues, - $this->mapPropertyNames, - $this->wrapExecutionType, - $this->partials->merge($partials), - ); - } - public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self { return new self( $this->transformValues, $this->mapPropertyNames, $wrapExecutionType, - $this->partials, + $this->includedPartials, + $this->excludedPartials, + $this->onlyPartials, + $this->exceptPartials, ); } } diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 76c8defa..8eba25ba 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -5,8 +5,9 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; +use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; +use SplObjectStorage; class TransformationContextFactory { @@ -18,26 +19,74 @@ public static function create(): self } /** - * @param bool $transformValues - * @param bool $mapPropertyNames - * @param \Spatie\LaravelData\Support\Wrapping\WrapExecutionType $wrapExecutionType + * @param SplObjectStorage $includedPartials + * @param SplObjectStorage $excludedPartials + * @param SplObjectStorage $onlyPartials + * @param SplObjectStorage $exceptPartials */ protected function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public PartialsDefinition $partialsDefinition = new PartialsDefinition(), + public SplObjectStorage $includedPartials = new SplObjectStorage(), + public SplObjectStorage $excludedPartials = new SplObjectStorage(), + public SplObjectStorage $onlyPartials = new SplObjectStorage(), + public SplObjectStorage $exceptPartials = new SplObjectStorage(), ) { } public function get( - BaseData|BaseDataCollectable $data - ): TransformationContext { + BaseData|BaseDataCollectable $data, + ): TransformationContext + { + $includedPartials = new SplObjectStorage(); + + foreach ($this->includedPartials as $include) { + $resolved = $include->resolve($data); + + if($resolved){ + $includedPartials->attach($resolved); + } + } + + $excludedPartials = new SplObjectStorage(); + + foreach ($this->excludedPartials as $exclude) { + $resolved = $exclude->resolve($data); + + if($resolved){ + $excludedPartials->attach($resolved); + } + } + + $onlyPartials = new SplObjectStorage(); + + foreach ($this->onlyPartials as $only) { + $resolved = $only->resolve($data); + + if($resolved){ + $onlyPartials->attach($resolved); + } + } + + $exceptPartials = new SplObjectStorage(); + + foreach ($this->exceptPartials as $except) { + $resolved = $except->resolve($data); + + if($resolved){ + $exceptPartials->attach($resolved); + } + } + return new TransformationContext( $this->transformValues, $this->mapPropertyNames, $this->wrapExecutionType, - PartialTransformationContext::create($data, $this->partialsDefinition), + $includedPartials, + $excludedPartials, + $onlyPartials, + $exceptPartials, ); } @@ -62,8 +111,84 @@ public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static return $this; } - protected function getPartialsDefinition(): PartialsDefinition + public function addIncludePartial(Partial ...$partial): static + { + foreach ($partial as $include) { + $this->includedPartials->attach($include); + } + + return $this; + } + + public function addExcludePartial(Partial ...$partial): static + { + foreach ($partial as $exclude) { + $this->excludedPartials->attach($exclude); + } + + return $this; + } + + public function addOnlyPartial(Partial ...$partial): static + { + foreach ($partial as $only) { + $this->onlyPartials->attach($only); + } + + return $this; + } + + public function addExceptPartial(Partial ...$partial): static + { + foreach ($partial as $except) { + $this->exceptPartials->attach($except); + } + + return $this; + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeIncludePartials(SplObjectStorage $partials): static + { + $this->includedPartials->addAll($partials); + + return $this; + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeExcludePartials(SplObjectStorage $partials): static { - return $this->partialsDefinition; + $this->excludedPartials->addAll($partials); + + return $this; + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeOnlyPartials(SplObjectStorage $partials): static + { + $this->onlyPartials->addAll($partials); + + return $this; + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeExceptPartials(SplObjectStorage $partials): static + { + $this->exceptPartials->addAll($partials); + + return $this; + } + + protected function getPartialsContainer(): object + { + return $this; } } diff --git a/src/Support/TreeNodes/AllTreeNode.php b/src/Support/TreeNodes/AllTreeNode.php deleted file mode 100644 index 5c8818b8..00000000 --- a/src/Support/TreeNodes/AllTreeNode.php +++ /dev/null @@ -1,31 +0,0 @@ - */ - protected array $children = [] - ) { - } - - public function merge(TreeNode $other): TreeNode - { - if ($other instanceof AllTreeNode) { - return $other; - } - - if ($other instanceof DisabledTreeNode || $other instanceof ExcludedTreeNode) { - return $this; - } - - if (! $other instanceof PartialTreeNode) { - throw new TypeError('Invalid node type'); - } - - $children = $this->children; - - foreach ($other->children as $otherField => $otherNode) { - if (array_key_exists($otherField, $children)) { - $children[$otherField] = $otherNode->merge($children[$otherField]); - - continue; - } - - $children[$otherField] = $otherNode; - } - - return new PartialTreeNode($children); - } - - public function intersect(TreeNode $other): TreeNode - { - if ($other instanceof AllTreeNode) { - return $this; - } - - if ($other instanceof DisabledTreeNode || $other instanceof ExcludedTreeNode) { - return $other; - } - - if ($other instanceof PartialTreeNode) { - $children = []; - - foreach ($other->children as $otherField => $otherNode) { - if (array_key_exists($otherField, $this->children)) { - $children[$otherField] = $this->children[$otherField]->intersect($otherNode); - } - } - - return new PartialTreeNode($children); - } - - throw new TypeError('Unknown tree node type'); - } - - public function getNested(string $field): TreeNode - { - return $this->children[$field] ?? new ExcludedTreeNode(); - } - - public function hasField(string $field): bool - { - return array_key_exists($field, $this->children); - } - - public function __toString(): string - { - return '{' . collect($this->children)->map(fn (TreeNode $child, string $field) => "\"{$field}\":{$child}")->join(',') . '}'; - } - - public function getFields(): array - { - return array_keys($this->children); - } -} diff --git a/src/Support/TreeNodes/TreeNode.php b/src/Support/TreeNodes/TreeNode.php deleted file mode 100644 index 346f4e47..00000000 --- a/src/Support/TreeNodes/TreeNode.php +++ /dev/null @@ -1,16 +0,0 @@ - $data->toUserDefinedToArray(), name: 'single', ); -}); +})->skip('Use PHPBench for now'); function benchSingle(Closure $closure, $times): float { diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 67d0071a..2577d2a8 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -179,6 +179,8 @@ it('can update data properties withing a collection', function () { + LazyData::setAllowedIncludes(null); + $collection = new DataCollection(LazyData::class, [ LazyData::from('Never gonna give you up!'), ]); diff --git a/tests/DataTest.php b/tests/DataTest.php index c306a2de..66a8b1e1 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1409,11 +1409,6 @@ public function __construct( } }; - $d = $dataClass::from([ - 'array' => ['A', 'B'], - 'collection' => ['A', 'B'], - ]); - expect($dataClass->include('array.name', 'collection.name')->toArray())->toBe([ 'array' => [ ['name' => 'A'], diff --git a/tests/Fakes/DefaultLazyData.php b/tests/Fakes/DefaultLazyData.php index cab1b404..dea75c47 100644 --- a/tests/Fakes/DefaultLazyData.php +++ b/tests/Fakes/DefaultLazyData.php @@ -4,10 +4,12 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; class DefaultLazyData extends Data { - public static ?array $allowedExcludes; + protected static ?array $allowedExcludes = null; public function __construct( public string | Lazy $name @@ -25,4 +27,12 @@ public static function allowedRequestExcludes(): ?array { return self::$allowedExcludes; } + + public static function setAllowedExcludes(?array $allowedExcludes): void + { + self::$allowedExcludes = $allowedExcludes; + + // Ensure cached config is cleared + app(DataConfig::class)->reset(); + DataContainer::get()->reset(); } } diff --git a/tests/Fakes/ExceptData.php b/tests/Fakes/ExceptData.php index 23b3f92f..caff0775 100644 --- a/tests/Fakes/ExceptData.php +++ b/tests/Fakes/ExceptData.php @@ -3,10 +3,12 @@ namespace Spatie\LaravelData\Tests\Fakes; use Spatie\LaravelData\Data; +use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; class ExceptData extends Data { - public static ?array $allowedExcept; + protected static ?array $allowedExcept = null; public function __construct( public string $first_name, @@ -18,4 +20,12 @@ public static function allowedRequestExcept(): ?array { return self::$allowedExcept; } + + public static function setAllowedExcept(?array $allowedExcept): void + { + self::$allowedExcept = $allowedExcept; + + // Ensure cached config is cleared + app(DataConfig::class)->reset(); + DataContainer::get()->reset(); } } diff --git a/tests/Fakes/LazyData.php b/tests/Fakes/LazyData.php index 7305dda1..0f7efed5 100644 --- a/tests/Fakes/LazyData.php +++ b/tests/Fakes/LazyData.php @@ -4,13 +4,15 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; class LazyData extends Data { - public static ?array $allowedIncludes; + protected static ?array $allowedIncludes = null; public function __construct( - public string | Lazy $name + public string|Lazy $name ) { } @@ -23,4 +25,13 @@ public static function allowedRequestIncludes(): ?array { return self::$allowedIncludes; } + + public static function setAllowedIncludes(?array $allowedIncludes): void + { + self::$allowedIncludes = $allowedIncludes; + + // Ensure cached config is cleared + app(DataConfig::class)->reset(); + DataContainer::get()->reset(); + } } diff --git a/tests/Fakes/OnlyData.php b/tests/Fakes/OnlyData.php index edbcb726..122511c8 100644 --- a/tests/Fakes/OnlyData.php +++ b/tests/Fakes/OnlyData.php @@ -3,10 +3,12 @@ namespace Spatie\LaravelData\Tests\Fakes; use Spatie\LaravelData\Data; +use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; class OnlyData extends Data { - public static ?array $allowedOnly; + protected static ?array $allowedOnly = null; public function __construct( public string $first_name, @@ -18,4 +20,13 @@ public static function allowedRequestOnly(): ?array { return self::$allowedOnly; } + + public static function setAllowedOnly(?array $allowedOnly): void + { + self::$allowedOnly = $allowedOnly; + + // Ensure cached config is cleared + app(DataConfig::class)->reset(); + DataContainer::get()->reset(); + } } diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 963aaf0c..56176a9b 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -8,6 +8,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; @@ -279,13 +280,13 @@ public static function create(string $name): static it('can dynamically include data based upon the request', function () { - LazyData::$allowedIncludes = []; + LazyData::setAllowedIncludes([]); $response = LazyData::from('Ruben')->toResponse(request()); expect($response)->getData(true)->toBe([]); - LazyData::$allowedIncludes = ['name']; + LazyData::setAllowedIncludes(['name']); $includedResponse = LazyData::from('Ruben')->toResponse(request()->merge([ 'include' => 'name', @@ -296,7 +297,7 @@ public static function create(string $name): static }); it('can disabled including data dynamically from the request', function () { - LazyData::$allowedIncludes = []; + LazyData::setAllowedIncludes([]); $response = LazyData::from('Ruben')->toResponse(request()->merge([ 'include' => 'name', @@ -304,7 +305,7 @@ public static function create(string $name): static expect($response->getData(true))->toBe([]); - LazyData::$allowedIncludes = ['name']; + LazyData::setAllowedIncludes(['name']); $response = LazyData::from('Ruben')->toResponse(request()->merge([ 'include' => 'name', @@ -312,7 +313,7 @@ public static function create(string $name): static expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); - LazyData::$allowedIncludes = null; + LazyData::setAllowedIncludes(null); $response = LazyData::from('Ruben')->toResponse(request()->merge([ 'include' => 'name', @@ -322,13 +323,13 @@ public static function create(string $name): static }); it('can dynamically exclude data based upon the request', function () { - DefaultLazyData::$allowedExcludes = []; + DefaultLazyData::setAllowedExcludes([]); $response = DefaultLazyData::from('Ruben')->toResponse(request()); expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); - DefaultLazyData::$allowedExcludes = ['name']; + DefaultLazyData::setAllowedExcludes(['name']); $excludedResponse = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ 'exclude' => 'name', @@ -338,7 +339,7 @@ public static function create(string $name): static }); it('can disable excluding data dynamically from the request', function () { - DefaultLazyData::$allowedExcludes = []; + DefaultLazyData::setAllowedExcludes([]); $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ 'exclude' => 'name', @@ -346,7 +347,7 @@ public static function create(string $name): static expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); - DefaultLazyData::$allowedExcludes = ['name']; + DefaultLazyData::setAllowedExcludes(['name']); $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ 'exclude' => 'name', @@ -354,7 +355,7 @@ public static function create(string $name): static expect($response->getData(true))->toBe([]); - DefaultLazyData::$allowedExcludes = null; + DefaultLazyData::setAllowedExcludes(null); $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ 'exclude' => 'name', @@ -364,7 +365,7 @@ public static function create(string $name): static }); it('can disable only data dynamically from the request', function () { - OnlyData::$allowedOnly = []; + OnlyData::setAllowedOnly([]); $response = OnlyData::from([ 'first_name' => 'Ruben', @@ -378,7 +379,7 @@ public static function create(string $name): static 'last_name' => 'Van Assche', ]); - OnlyData::$allowedOnly = ['first_name']; + OnlyData::setAllowedOnly(['first_name']); $response = OnlyData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ 'only' => 'first_name', @@ -388,7 +389,7 @@ public static function create(string $name): static 'first_name' => 'Ruben', ]); - OnlyData::$allowedOnly = null; + OnlyData::setAllowedOnly(null); $response = OnlyData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ 'only' => 'first_name', @@ -400,7 +401,7 @@ public static function create(string $name): static }); it('can disable except data dynamically from the request', function () { - ExceptData::$allowedExcept = []; + ExceptData::setAllowedExcept([]); $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ 'except' => 'first_name', @@ -411,7 +412,7 @@ public static function create(string $name): static 'last_name' => 'Van Assche', ]); - ExceptData::$allowedExcept = ['first_name']; + ExceptData::setAllowedExcept(['first_name']); $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ 'except' => 'first_name', @@ -421,7 +422,7 @@ public static function create(string $name): static 'last_name' => 'Van Assche', ]); - ExceptData::$allowedExcept = null; + ExceptData::setAllowedExcept(null); $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ 'except' => 'first_name', @@ -520,6 +521,7 @@ public function __construct( ->toMatchArray(['enabled' => true, 'nested' => ['string' => 'Hello World']]); }); + it('can conditionally include using class defaults multiple', function () { PartialClassConditionalData::setDefinitions(includeDefinitions: [ 'nested.string' => fn (PartialClassConditionalData $data) => $data->enabled, @@ -878,7 +880,7 @@ public function __construct( ]); }); -test('only has precedence over except', function () { +test('except has precedence over only', function () { $data = new MultiData('Hello', 'World'); expect( @@ -890,7 +892,7 @@ public function __construct( expect( (clone $data)->exceptWhen('first', true)->onlyWhen('first', true)->toArray() )->toMatchArray(['second' => 'World']); -}); +})->skip('We know first perform except and then only, test can be removed'); it('can perform only and except on array properties', function () { $data = new class ('Hello World', ['string' => 'Hello World', 'int' => 42]) extends Data { @@ -944,7 +946,7 @@ public function __construct( expect($data->fakeModel->string)->toBe('lazy'); }); -it('has array access and will replicate partialtrees (collection)', function () { +it('has array access and will replicate partials (collection)', function () { $collection = MultiData::collect([ new MultiData('first', 'second'), ], DataCollection::class)->only('second'); @@ -953,7 +955,7 @@ public function __construct( }); it('can dynamically include data based upon the request (collection)', function () { - LazyData::$allowedIncludes = ['']; + LazyData::setAllowedIncludes(['']); $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); @@ -964,7 +966,7 @@ public function __construct( [], ]); - LazyData::$allowedIncludes = ['name']; + LazyData::setAllowedIncludes(['name']); $includedResponse = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', @@ -979,7 +981,7 @@ public function __construct( }); it('can disable manually including data in the request (collection)', function () { - LazyData::$allowedIncludes = []; + LazyData::setAllowedIncludes([]); $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', @@ -992,7 +994,7 @@ public function __construct( [], ]); - LazyData::$allowedIncludes = ['name']; + LazyData::setAllowedIncludes(['name']); $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', @@ -1005,7 +1007,7 @@ public function __construct( ['name' => 'Brent'], ]); - LazyData::$allowedIncludes = null; + LazyData::setAllowedIncludes(null); $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'include' => 'name', @@ -1020,7 +1022,7 @@ public function __construct( }); it('can dynamically exclude data based upon the request (collection)', function () { - DefaultLazyData::$allowedExcludes = []; + DefaultLazyData::setAllowedExcludes([]); $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()); @@ -1031,7 +1033,7 @@ public function __construct( ['name' => 'Brent'], ]); - DefaultLazyData::$allowedExcludes = ['name']; + DefaultLazyData::setAllowedExcludes(['name']); $excludedResponse = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', @@ -1046,7 +1048,7 @@ public function __construct( }); it('can disable manually excluding data in the request (collection)', function () { - DefaultLazyData::$allowedExcludes = []; + DefaultLazyData::setAllowedExcludes([]); $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', @@ -1059,7 +1061,7 @@ public function __construct( ['name' => 'Brent'], ]); - DefaultLazyData::$allowedExcludes = ['name']; + DefaultLazyData::setAllowedExcludes(['name']); $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', @@ -1072,7 +1074,7 @@ public function __construct( [], ]); - DefaultLazyData::$allowedExcludes = null; + DefaultLazyData::setAllowedExcludes(null); $response = (new DataCollection(DefaultLazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ 'exclude' => 'name', diff --git a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php index e33ee4a8..e4338591 100644 --- a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php +++ b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php @@ -16,155 +16,152 @@ use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedOutputName; use Spatie\LaravelData\Tests\Fakes\UlarData; -beforeEach(function () { - $this->resolver = resolve(PartialsTreeFromRequestResolver::class); -}); - -it('will correctly reduce a tree based upon allowed includes', function ( - ?array $lazyDataAllowedIncludes, - ?array $dataAllowedIncludes, - ?string $requestedAllowedIncludes, - TreeNode $expectedIncludes -) { - LazyData::$allowedIncludes = $lazyDataAllowedIncludes; - - $data = new class ( - 'Hello', - LazyData::from('Hello'), - LazyData::collect(['Hello', 'World']) - ) extends Data { - public static ?array $allowedIncludes; - - public function __construct( - public string $property, - public LazyData $nested, - #[DataCollectionOf(LazyData::class)] - public array $collection, - ) { - } - - public static function allowedRequestIncludes(): ?array - { - return static::$allowedIncludes; - } - }; - - $data::$allowedIncludes = $dataAllowedIncludes; - - $request = request(); - - if ($requestedAllowedIncludes !== null) { - $request->merge([ - 'include' => $requestedAllowedIncludes, - ]); - } - - $trees = $this->resolver->execute($data, $request); - - expect($trees->lazyIncluded)->toEqual($expectedIncludes); -})->with(function () { - yield 'disallowed property inclusion' => [ - 'lazyDataAllowedIncludes' => [], - 'dataAllowedIncludes' => [], - 'requestedIncludes' => 'property', - 'expectedIncludes' => new ExcludedTreeNode(), - ]; - - yield 'allowed property inclusion' => [ - 'lazyDataAllowedIncludes' => [], - 'dataAllowedIncludes' => ['property'], - 'requestedIncludes' => 'property', - 'expectedIncludes' => new PartialTreeNode([ - 'property' => new ExcludedTreeNode(), - ]), - ]; - - yield 'allowed data property inclusion without nesting' => [ - 'lazyDataAllowedIncludes' => [], - 'dataAllowedIncludes' => ['nested'], - 'requestedIncludes' => 'nested.name', - 'expectedIncludes' => new PartialTreeNode([ - 'nested' => new ExcludedTreeNode(), - ]), - ]; - - yield 'allowed data property inclusion with nesting' => [ - 'lazyDataAllowedIncludes' => ['name'], - 'dataAllowedIncludes' => ['nested'], - 'requestedIncludes' => 'nested.name', - 'expectedIncludes' => new PartialTreeNode([ - 'nested' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield 'allowed data collection property inclusion without nesting' => [ - 'lazyDataAllowedIncludes' => [], - 'dataAllowedIncludes' => ['collection'], - 'requestedIncludes' => 'collection.name', - 'expectedIncludes' => new PartialTreeNode([ - 'collection' => new ExcludedTreeNode(), - ]), - ]; - - yield 'allowed data collection property inclusion with nesting' => [ - 'lazyDataAllowedIncludes' => ['name'], - 'dataAllowedIncludes' => ['collection'], - 'requestedIncludes' => 'collection.name', - 'expectedIncludes' => new PartialTreeNode([ - 'collection' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ - 'lazyDataAllowedIncludes' => null, - 'dataAllowedIncludes' => ['nested'], - 'requestedIncludes' => 'nested.name', - 'expectedIncludes' => new PartialTreeNode([ - 'nested' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ - 'lazyDataAllowedIncludes' => null, - 'dataAllowedIncludes' => ['nested'], - 'requestedIncludes' => 'nested.*', - 'expectedIncludes' => new PartialTreeNode([ - 'nested' => new AllTreeNode(), - ]), - ]; - - yield 'disallowed all nested data property inclusion ' => [ - 'lazyDataAllowedIncludes' => [], - 'dataAllowedIncludes' => ['nested'], - 'requestedIncludes' => 'nested.*', - 'expectedIncludes' => new PartialTreeNode([ - 'nested' => new ExcludedTreeNode(), - ]), - ]; - - yield 'multi property inclusion' => [ - 'lazyDataAllowedIncludes' => null, - 'dataAllowedIncludes' => ['nested', 'property'], - 'requestedIncludes' => 'nested.*,property', - 'expectedIncludes' => new PartialTreeNode([ - 'property' => new ExcludedTreeNode(), - 'nested' => new AllTreeNode(), - ]), - ]; - - yield 'without property inclusion' => [ - 'lazyDataAllowedIncludes' => null, - 'dataAllowedIncludes' => ['nested', 'property'], - 'requestedIncludes' => null, - 'expectedIncludes' => new DisabledTreeNode(), - ]; -}); +// Todo: replace +//it('will correctly reduce a tree based upon allowed includes', function ( +// ?array $lazyDataAllowedIncludes, +// ?array $dataAllowedIncludes, +// ?string $requestedAllowedIncludes, +// TreeNode $expectedIncludes +//) { +// LazyData::setAllowedIncludes($lazyDataAllowedIncludes); +// +// $data = new class ( +// 'Hello', +// LazyData::from('Hello'), +// LazyData::collect(['Hello', 'World']) +// ) extends Data { +// public static ?array $allowedIncludes; +// +// public function __construct( +// public string $property, +// public LazyData $nested, +// #[DataCollectionOf(LazyData::class)] +// public array $collection, +// ) { +// } +// +// public static function allowedRequestIncludes(): ?array +// { +// return static::$allowedIncludes; +// } +// }; +// +// $data::$allowedIncludes = $dataAllowedIncludes; +// +// $request = request(); +// +// if ($requestedAllowedIncludes !== null) { +// $request->merge([ +// 'include' => $requestedAllowedIncludes, +// ]); +// } +// +// $trees = $this->resolver->execute($data, $request); +// +// expect($trees->lazyIncluded)->toEqual($expectedIncludes); +//})->with(function () { +// yield 'disallowed property inclusion' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => [], +// 'requestedIncludes' => 'property', +// 'expectedIncludes' => new ExcludedTreeNode(), +// ]; +// +// yield 'allowed property inclusion' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['property'], +// 'requestedIncludes' => 'property', +// 'expectedIncludes' => new PartialTreeNode([ +// 'property' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data property inclusion without nesting' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data property inclusion with nesting' => [ +// 'lazyDataAllowedIncludes' => ['name'], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed data collection property inclusion without nesting' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['collection'], +// 'requestedIncludes' => 'collection.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'collection' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data collection property inclusion with nesting' => [ +// 'lazyDataAllowedIncludes' => ['name'], +// 'dataAllowedIncludes' => ['collection'], +// 'requestedIncludes' => 'collection.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'collection' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.*', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new AllTreeNode(), +// ]), +// ]; +// +// yield 'disallowed all nested data property inclusion ' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.*', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'multi property inclusion' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested', 'property'], +// 'requestedIncludes' => 'nested.*,property', +// 'expectedIncludes' => new PartialTreeNode([ +// 'property' => new ExcludedTreeNode(), +// 'nested' => new AllTreeNode(), +// ]), +// ]; +// +// yield 'without property inclusion' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested', 'property'], +// 'requestedIncludes' => null, +// 'expectedIncludes' => new DisabledTreeNode(), +// ]; +//}); it('can combine request and manual includes', function () { $dataclass = new class ( diff --git a/tests/Support/DataClassTest.php b/tests/Support/DataClassTest.php index b59a46d9..61724a0c 100644 --- a/tests/Support/DataClassTest.php +++ b/tests/Support/DataClassTest.php @@ -81,15 +81,6 @@ public function __construct( ->and(ModelWithPhpStormAttributeData::from((new DummyModel())->fill(['id' => 1]))->id)->toEqual(1); }); -it('wont create an output name mapping for non mapped properties', function () { - $mapping = DataClass::create(new ReflectionClass(SimpleData::class)) - ->outputNameMapping; - - expect($mapping) - ->mapped->toBeEmpty() - ->mappedDataObjects->toBeEmpty(); -}); - it('resolves parent attributes', function () { #[MapName(SnakeCaseMapper::class)] #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] diff --git a/tests/Support/NameMapping/DataClassNameMappingTest.php b/tests/Support/NameMapping/DataClassNameMappingTest.php deleted file mode 100644 index 17024375..00000000 --- a/tests/Support/NameMapping/DataClassNameMappingTest.php +++ /dev/null @@ -1,41 +0,0 @@ -getDataClass($dataClass::class) - ->outputNameMapping; - - expect($mapping->getOriginal('non_mapped'))->toBeNull(); - expect($mapping->getOriginal('naam'))->toBe('name'); - expect($mapping->getOriginal('genest'))->toBe('nested'); - - expect($mapping->resolveNextMapping(app(DataConfig::class), 'nested')) - ->toBeInstanceOf(DataClassNameMapping::class); - - expect($mapping->resolveNextMapping(app(DataConfig::class), 'nested_with_mapping')) - ->toBeInstanceOf(DataClassNameMapping::class) - ->getOriginal('paid_amount')->toBe('amount') - ->getOriginal('any_string')->toBe('anyString') - ->getOriginal('child')->toBe('child'); -}); diff --git a/tests/Support/Partials/PartialTest.php b/tests/Support/Partials/PartialTest.php new file mode 100644 index 00000000..3dec8ba1 --- /dev/null +++ b/tests/Support/Partials/PartialTest.php @@ -0,0 +1,74 @@ +segments)->toEqual($segments); +})->with(function () { + yield from rootPartialsProvider(); + yield from nestedPartialsProvider(); + yield from invalidPartialsProvider(); +}); + +function rootPartialsProvider(): Generator +{ + yield "empty" => [ + 'partials' => '', + 'expected' => [], + ]; + + yield "root property" => [ + 'partials' => 'name', + 'expected' => [new FieldsPartialSegment(['name'])], + ]; + + yield "root multi-property" => [ + 'partials' => '{name, age}', + 'expected' => [new FieldsPartialSegment(['name', 'age'])], + ]; + + yield "root star" => [ + 'partials' => '*', + 'expected' => [new AllPartialSegment()], + ]; +} + +function nestedPartialsProvider(): Generator +{ + yield "nested property" => [ + 'partials' => 'struct.name', + 'expected' => [new NestedPartialSegment('struct'), new FieldsPartialSegment(['name'])] + ]; + + yield "nested multi-property" => [ + 'partials' => 'struct.{name, age}', + 'expected' => [new NestedPartialSegment('struct'), new FieldsPartialSegment(['name', 'age'])] + ]; + + yield "nested star" => [ + 'partials' => 'struct.*', + 'expected' => [new NestedPartialSegment('struct'), new AllPartialSegment()] + ]; +} + +function invalidPartialsProvider(): Generator +{ + yield "nested property on all" => [ + 'partials' => '*.name', + 'expected' => [new AllPartialSegment()], + ]; + + yield "nested property on multi-property" => [ + 'partials' => '{name, age}.name', + 'expected' => [new FieldsPartialSegment(['name', 'age'])] + ]; +} diff --git a/tests/Support/PartialsParserTest.php b/tests/Support/PartialsParserTest.php deleted file mode 100644 index 4f3ed56d..00000000 --- a/tests/Support/PartialsParserTest.php +++ /dev/null @@ -1,268 +0,0 @@ -execute($partials)->toEqual($expected); -})->with(function () { - yield from rootPartialsProvider(); - yield from nestedPartialsProvider(); - yield from invalidPartialsProvider(); - yield from complexPartialsProvider(); -}); - - -function rootPartialsProvider(): Generator -{ - yield "empty" => [ - 'partials' => [], - 'expected' => new DisabledTreeNode(), - ]; - - yield "root property" => [ - 'partials' => [ - 'name', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - ]), - ]; - - yield "root multi-property" => [ - 'partials' => [ - '{name, age}', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - ]), - ]; - - yield "root star" => [ - 'partials' => [ - '*', - ], - 'expected' => new AllTreeNode(), - ]; - - yield "root star overrules" => [ - 'partials' => [ - 'name', - '*', - 'age', - ], - 'expected' => new AllTreeNode(), - ]; - - yield "root combination" => [ - 'partials' => [ - 'name', - '{name, age}', - 'age', - 'gender', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - 'gender' => new ExcludedTreeNode(), - ]), - ]; -} - -function nestedPartialsProvider(): Generator -{ - yield "nested property" => [ - 'partials' => [ - 'struct.name', - ], - 'expected' => new PartialTreeNode([ - 'struct' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield "nested multi-property" => [ - 'partials' => [ - 'struct.{name, age}', - ], - 'expected' => new PartialTreeNode([ - 'struct' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield "nested star" => [ - 'partials' => [ - 'struct.*', - ], - 'expected' => new PartialTreeNode([ - 'struct' => new AllTreeNode(), - ]), - ]; - - yield "nested star overrules" => [ - 'partials' => [ - 'struct.name', - 'struct.*', - 'struct.age', - ], - 'expected' => new PartialTreeNode([ - 'struct' => new AllTreeNode(), - ]), - ]; - - yield "nested combination" => [ - 'partials' => [ - 'struct.name', - 'struct.{name, age}', - 'struct.age', - 'struct.gender', - ], - 'expected' => new PartialTreeNode([ - 'struct' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - 'gender' => new ExcludedTreeNode(), - ]), - ]), - ]; -} - -function invalidPartialsProvider(): Generator -{ - yield "nested property on all" => [ - 'partials' => [ - '*.name', - ], - 'expected' => new AllTreeNode(), - ]; - - yield "nested property on multi-property" => [ - 'partials' => [ - '{name, age}.name', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - ]), - ]; -} - -function complexPartialsProvider(): Generator -{ - yield "a complex example" => [ - 'partials' => [ - 'name', - 'age', - 'posts.name', - 'posts.tags.*', - 'identities.auth0.{name,email}', - 'books.title', - 'books.*', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - 'posts' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'tags' => new AllTreeNode(), - ]), - 'identities' => new PartialTreeNode([ - 'auth0' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'email' => new ExcludedTreeNode(), - ]), - ]), - 'books' => new AllTreeNode(), - ]), - ]; -} - -it('can parse directives with mapping', function (array $partials, TreeNode $expected) { - $fakeClass = new class () extends Data { - #[MapOutputName('naam')] - public string $name; - - #[MapOutputName('leeftijd')] - public string $age; - - #[MapOutputName('geslacht')] - public string $gender; - - #[MapOutputName('structuur')] - public SimpleDataWithMappedOutputName $struct; - }; - - $mapping = app(DataConfig::class)->getDataClass($fakeClass::class)->outputNameMapping; - - expect(app(PartialsParser::class)) - ->execute($partials, $mapping) - ->toEqual($expected); -})->with(function () { - yield "empty" => [ - 'partials' => [], - 'expected' => new DisabledTreeNode(), - ]; - - yield "all mapped" => [ - 'partials' => [ - 'naam', - '{leeftijd, geslacht}', - 'structuur.any_string', - 'structuur.{paid_amount}', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - 'gender' => new ExcludedTreeNode(), - 'struct' => new PartialTreeNode([ - 'anyString' => new ExcludedTreeNode(), - 'amount' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield "some mapped, some not + non defined mappings" => [ - 'partials' => [ - 'name', - 'bio', - '{leeftijd, gender}', - 'structuur.anyString', - 'struct.id', - 'structuur.{paid_amount, child}', - ], - 'expected' => new PartialTreeNode([ - 'name' => new ExcludedTreeNode(), - 'bio' => new ExcludedTreeNode(), - 'age' => new ExcludedTreeNode(), - 'gender' => new ExcludedTreeNode(), - 'struct' => new PartialTreeNode([ - 'id' => new ExcludedTreeNode(), - 'anyString' => new ExcludedTreeNode(), - 'amount' => new ExcludedTreeNode(), - 'child' => new ExcludedTreeNode(), - ]), - ]), - ]; - - yield "star operator" => [ - 'partials' => [ - 'structuur.*', - ], - 'expected' => new PartialTreeNode([ - 'struct' => new AllTreeNode(), - ]), - ]; -}); diff --git a/tests/Support/TreeNodes/AllTreeNodeTest.php b/tests/Support/TreeNodes/AllTreeNodeTest.php deleted file mode 100644 index 39b95ec1..00000000 --- a/tests/Support/TreeNodes/AllTreeNodeTest.php +++ /dev/null @@ -1,34 +0,0 @@ -toEqual($node->merge(new AllTreeNode())) - ->toEqual($node->merge(new ExcludedTreeNode())) - ->toEqual($node->merge(new DisabledTreeNode())) - ->toEqual($node->merge(new PartialTreeNode())) - ->toEqual($node->merge(new PartialTreeNode())) - ->toEqual($node->merge(new PartialTreeNode([ - 'nested' => new ExcludedTreeNode(), - ]))); -}); - -it('can intersect a node', function () { - $node = new AllTreeNode(); - - expect($node)->toEqual($node->intersect(new AllTreeNode())) - ->and($node->intersect(new ExcludedTreeNode()))->toEqual(new ExcludedTreeNode()) - ->and($node->intersect(new DisabledTreeNode()))->toEqual(new DisabledTreeNode()) - ->and($node->intersect(new PartialTreeNode()))->toEqual(new PartialTreeNode()) - ->and($node->intersect(new ExcludedTreeNode()))->toEqual(new ExcludedTreeNode()) - ->and( - $node->intersect( - new PartialTreeNode(['nested' => new ExcludedTreeNode()]) - ) - )->toEqual(new PartialTreeNode(['nested' => new ExcludedTreeNode()])); -}); diff --git a/tests/Support/TreeNodes/DisabledTreeNodeTest.php b/tests/Support/TreeNodes/DisabledTreeNodeTest.php deleted file mode 100644 index 7bc2cb25..00000000 --- a/tests/Support/TreeNodes/DisabledTreeNodeTest.php +++ /dev/null @@ -1,32 +0,0 @@ -merge(new AllTreeNode()))->toEqual(new AllTreeNode()) - ->and($node->merge(new ExcludedTreeNode()))->toEqual(new ExcludedTreeNode()) - ->and($node->merge(new DisabledTreeNode()))->toEqual(new DisabledTreeNode()) - ->and($node->merge(new PartialTreeNode()))->toEqual(new PartialTreeNode()) - ->and( - $node->merge(new PartialTreeNode(['nested' => new ExcludedTreeNode()])) - )->toEqual( - new PartialTreeNode(['nested' => new ExcludedTreeNode()]) - ); -}); - -it('can intersect a node', function () { - $node = new DisabledTreeNode(); - - expect($node->intersect(new AllTreeNode()))->toEqual($node) - ->and($node->intersect(new ExcludedTreeNode()))->toEqual($node) - ->and($node->intersect(new DisabledTreeNode()))->toEqual($node) - ->and($node->intersect(new PartialTreeNode()))->toEqual($node) - ->and( - $node->intersect(new PartialTreeNode(['nested' => new ExcludedTreeNode()])) - )->toEqual($node); -}); diff --git a/tests/Support/TreeNodes/ExcludedTreeNodeTest.php b/tests/Support/TreeNodes/ExcludedTreeNodeTest.php deleted file mode 100644 index c54f3246..00000000 --- a/tests/Support/TreeNodes/ExcludedTreeNodeTest.php +++ /dev/null @@ -1,33 +0,0 @@ -merge(new AllTreeNode()))->toEqual(new AllTreeNode()) - ->and($node->merge(new ExcludedTreeNode()))->toEqual($node) - ->and($node->merge(new DisabledTreeNode())) - ->toEqual(new DisabledTreeNode()) - ->and($node->merge(new PartialTreeNode())) - ->toEqual(new PartialTreeNode()) - ->and( - $node->merge(new PartialTreeNode(['nested' => new ExcludedTreeNode()])) - ) - ->toEqual(new PartialTreeNode(['nested' => new ExcludedTreeNode()])); -}); - -it('can intersect a node', function () { - $node = new ExcludedTreeNode(); - - expect($node->intersect(new AllTreeNode()))->toEqual($node) - ->and($node->intersect(new ExcludedTreeNode()))->toEqual($node) - ->and($node->intersect(new DisabledTreeNode()))->toEqual($node) - ->and($node->intersect(new PartialTreeNode()))->toEqual($node) - ->and( - $node->intersect(new PartialTreeNode(['nested' => new ExcludedTreeNode()])) - )->toEqual($node); -}); diff --git a/tests/Support/TreeNodes/PartialTreeNodeTest.php b/tests/Support/TreeNodes/PartialTreeNodeTest.php deleted file mode 100644 index 737978d9..00000000 --- a/tests/Support/TreeNodes/PartialTreeNodeTest.php +++ /dev/null @@ -1,54 +0,0 @@ - new ExcludedTreeNode(), - ]); - - expect($node->merge(new AllTreeNode())) - ->toEqual(new AllTreeNode()) - ->and($node->merge(new ExcludedTreeNode())) - ->toEqual($node) - ->and($node->merge(new DisabledTreeNode())) - ->toEqual($node) - ->and($node->merge(new PartialTreeNode())) - ->toEqual($node) - ->and( - $node->merge(new PartialTreeNode(['nested' => new ExcludedTreeNode()])) - ) - ->toEqual( - new PartialTreeNode([ - 'item' => new ExcludedTreeNode(), - 'nested' => new ExcludedTreeNode(), - ]) - ); -}); - -it('can intersect a node', function () { - $node = new PartialTreeNode([ - 'item' => new ExcludedTreeNode(), - ]); - - expect($node->intersect(new AllTreeNode()))->toEqual($node) - ->and($node->intersect(new ExcludedTreeNode())) - ->toEqual(new ExcludedTreeNode()) - ->and($node->intersect(new DisabledTreeNode())) - ->toEqual(new DisabledTreeNode()) - ->and($node->intersect(new PartialTreeNode())) - ->toEqual(new PartialTreeNode()) - ->and( - $node->intersect(new PartialTreeNode(['nested' => new ExcludedTreeNode()])) - ) - ->toEqual(new PartialTreeNode()) - ->and( - $node->intersect(new PartialTreeNode(['item' => new ExcludedTreeNode()])) - ) - ->toEqual( - new PartialTreeNode(['item' => new ExcludedTreeNode()]) - ); -}); From ed8d9e4640809a7b6ef33c936b13ea932f275965 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 5 Jan 2024 10:48:39 +0000 Subject: [PATCH 044/124] Fix styling --- src/Concerns/IncludeableData.php | 3 --- src/Concerns/ResponsableData.php | 2 -- .../TransformedDataCollectionResolver.php | 2 -- src/Resolvers/VisibleDataFieldsResolver.php | 2 +- src/Support/DataClass.php | 3 --- .../Partials/ForwardsToPartialsDefinition.php | 8 ++++---- src/Support/Partials/PartialType.php | 1 - src/Support/Partials/Segments/PartialSegment.php | 1 - .../Transformation/TransformationContext.php | 3 --- .../TransformationContextFactory.php | 11 +++++------ tests/Commands/DataStructuresCacheCommandTest.php | 4 ---- tests/Fakes/DefaultLazyData.php | 3 ++- tests/Fakes/ExceptData.php | 3 ++- tests/PartialsTest.php | 1 - .../PartialsTreeFromRequestResolverTest.php | 1 - tests/Support/Partials/PartialTest.php | 14 ++++---------- 16 files changed, 18 insertions(+), 44 deletions(-) diff --git a/src/Concerns/IncludeableData.php b/src/Concerns/IncludeableData.php index c89490b8..1d33736d 100644 --- a/src/Concerns/IncludeableData.php +++ b/src/Concerns/IncludeableData.php @@ -3,9 +3,6 @@ namespace Spatie\LaravelData\Concerns; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; -use Spatie\LaravelData\Support\Partials\Partial; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; -use SplObjectStorage; trait IncludeableData { diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 6a07408e..fd95d501 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -6,9 +6,7 @@ use Illuminate\Http\Request; use Illuminate\Http\Response; use Spatie\LaravelData\Support\DataContainer; -use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Partials\PartialType; -use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index dd7a2ccc..0bd4dded 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -15,8 +15,6 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataContainer; -use Spatie\LaravelData\Support\Transformation\PartialTransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index fe0c04af..ace1cf91 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -95,7 +95,7 @@ public function execute( } if ($value instanceof RelationalLazy || $value instanceof ConditionalLazy) { - if(! $value->shouldBeIncluded()){ + if(! $value->shouldBeIncluded()) { unset($fields[$field]); } diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 0316d84e..9f02a042 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Support; -use Hoa\Compiler\Llk\TreeNode; use Illuminate\Support\Collection; use ReflectionAttribute; use ReflectionClass; @@ -19,10 +18,8 @@ use Spatie\LaravelData\Contracts\ValidateableData; use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Mappers\ProvidedNameMapper; -use Spatie\LaravelData\Resolvers\AllowedRequestPartialsResolver; use Spatie\LaravelData\Resolvers\NameMappersResolver; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; -use Spatie\LaravelData\Support\NameMapping\DataClassNameMapping; /** * @property class-string $name diff --git a/src/Support/Partials/ForwardsToPartialsDefinition.php b/src/Support/Partials/ForwardsToPartialsDefinition.php index cbb4eacd..96d20972 100644 --- a/src/Support/Partials/ForwardsToPartialsDefinition.php +++ b/src/Support/Partials/ForwardsToPartialsDefinition.php @@ -57,7 +57,7 @@ public function includeWhen(string $include, bool|Closure $condition): static { if (is_callable($condition)) { $this->getPartialsContainer()->includePartials->attach(Partial::createConditional($include, $condition)); - } else if ($condition === true) { + } elseif ($condition === true) { $this->getPartialsContainer()->includePartials->attach(Partial::create($include)); } @@ -68,7 +68,7 @@ public function excludeWhen(string $exclude, bool|Closure $condition): static { if (is_callable($condition)) { $this->getPartialsContainer()->excludePartials->attach(Partial::createConditional($exclude, $condition)); - } else if ($condition === true) { + } elseif ($condition === true) { $this->getPartialsContainer()->excludePartials->attach(Partial::create($exclude)); } @@ -79,7 +79,7 @@ public function onlyWhen(string $only, bool|Closure $condition): static { if (is_callable($condition)) { $this->getPartialsContainer()->onlyPartials->attach(Partial::createConditional($only, $condition)); - } else if ($condition === true) { + } elseif ($condition === true) { $this->getPartialsContainer()->onlyPartials->attach(Partial::create($only)); } @@ -90,7 +90,7 @@ public function exceptWhen(string $except, bool|Closure $condition): static { if (is_callable($condition)) { $this->getPartialsContainer()->exceptPartials->attach(Partial::createConditional($except, $condition)); - } else if ($condition === true) { + } elseif ($condition === true) { $this->getPartialsContainer()->exceptPartials->attach(Partial::create($except)); } diff --git a/src/Support/Partials/PartialType.php b/src/Support/Partials/PartialType.php index 9fe499a7..9d9cf64f 100644 --- a/src/Support/Partials/PartialType.php +++ b/src/Support/Partials/PartialType.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Support\Partials; -use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Support\DataClass; enum PartialType diff --git a/src/Support/Partials/Segments/PartialSegment.php b/src/Support/Partials/Segments/PartialSegment.php index 83b4d132..8b90e537 100644 --- a/src/Support/Partials/Segments/PartialSegment.php +++ b/src/Support/Partials/Segments/PartialSegment.php @@ -6,5 +6,4 @@ abstract class PartialSegment implements Stringable { - } diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 69542707..1808a5a6 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -2,9 +2,6 @@ namespace Spatie\LaravelData\Support\Transformation; -use Exception; -use Spatie\LaravelData\Support\Partials\Partial; -use Spatie\LaravelData\Support\Partials\PartialsDefinition; use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use SplObjectStorage; diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 8eba25ba..98a9a1f3 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -37,14 +37,13 @@ protected function __construct( public function get( BaseData|BaseDataCollectable $data, - ): TransformationContext - { + ): TransformationContext { $includedPartials = new SplObjectStorage(); foreach ($this->includedPartials as $include) { $resolved = $include->resolve($data); - if($resolved){ + if($resolved) { $includedPartials->attach($resolved); } } @@ -54,7 +53,7 @@ public function get( foreach ($this->excludedPartials as $exclude) { $resolved = $exclude->resolve($data); - if($resolved){ + if($resolved) { $excludedPartials->attach($resolved); } } @@ -64,7 +63,7 @@ public function get( foreach ($this->onlyPartials as $only) { $resolved = $only->resolve($data); - if($resolved){ + if($resolved) { $onlyPartials->attach($resolved); } } @@ -74,7 +73,7 @@ public function get( foreach ($this->exceptPartials as $except) { $resolved = $except->resolve($data); - if($resolved){ + if($resolved) { $exceptPartials->attach($resolved); } } diff --git a/tests/Commands/DataStructuresCacheCommandTest.php b/tests/Commands/DataStructuresCacheCommandTest.php index 4d9cb1fa..3e538f0c 100644 --- a/tests/Commands/DataStructuresCacheCommandTest.php +++ b/tests/Commands/DataStructuresCacheCommandTest.php @@ -3,10 +3,6 @@ use Illuminate\Support\Facades\App; use Spatie\LaravelData\Support\Caching\CachedDataConfig; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; -use Spatie\LaravelData\Tests\Fakes\ExceptData; -use Spatie\LaravelData\Tests\Fakes\LazyData; -use Spatie\LaravelData\Tests\Fakes\OnlyData; use Spatie\LaravelData\Tests\Fakes\SimpleData; it('can cache data structures', function () { diff --git a/tests/Fakes/DefaultLazyData.php b/tests/Fakes/DefaultLazyData.php index dea75c47..2dbafea2 100644 --- a/tests/Fakes/DefaultLazyData.php +++ b/tests/Fakes/DefaultLazyData.php @@ -34,5 +34,6 @@ public static function setAllowedExcludes(?array $allowedExcludes): void // Ensure cached config is cleared app(DataConfig::class)->reset(); - DataContainer::get()->reset(); } + DataContainer::get()->reset(); + } } diff --git a/tests/Fakes/ExceptData.php b/tests/Fakes/ExceptData.php index caff0775..219dfe34 100644 --- a/tests/Fakes/ExceptData.php +++ b/tests/Fakes/ExceptData.php @@ -27,5 +27,6 @@ public static function setAllowedExcept(?array $allowedExcept): void // Ensure cached config is cleared app(DataConfig::class)->reset(); - DataContainer::get()->reset(); } + DataContainer::get()->reset(); + } } diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 56176a9b..a9d61af3 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -8,7 +8,6 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; -use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; diff --git a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php index e4338591..ac0ef765 100644 --- a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php +++ b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php @@ -3,7 +3,6 @@ use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Data; use Spatie\LaravelData\Lazy; -use Spatie\LaravelData\Resolvers\PartialsTreeFromRequestResolver; use Spatie\LaravelData\Support\TreeNodes\AllTreeNode; use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; diff --git a/tests/Support/Partials/PartialTest.php b/tests/Support/Partials/PartialTest.php index 3dec8ba1..51bcf23e 100644 --- a/tests/Support/Partials/PartialTest.php +++ b/tests/Support/Partials/PartialTest.php @@ -4,12 +4,6 @@ use Spatie\LaravelData\Support\Partials\Segments\AllPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\FieldsPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\NestedPartialSegment; -use Spatie\LaravelData\Support\PartialsParser; -use Spatie\LaravelData\Support\TreeNodes\AllTreeNode; -use Spatie\LaravelData\Support\TreeNodes\DisabledTreeNode; -use Spatie\LaravelData\Support\TreeNodes\ExcludedTreeNode; -use Spatie\LaravelData\Support\TreeNodes\PartialTreeNode; -use Spatie\LaravelData\Support\TreeNodes\TreeNode; it('can parse partials', function (string $partialString, array $segments) { expect(Partial::create($partialString)->segments)->toEqual($segments); @@ -46,17 +40,17 @@ function nestedPartialsProvider(): Generator { yield "nested property" => [ 'partials' => 'struct.name', - 'expected' => [new NestedPartialSegment('struct'), new FieldsPartialSegment(['name'])] + 'expected' => [new NestedPartialSegment('struct'), new FieldsPartialSegment(['name'])], ]; yield "nested multi-property" => [ 'partials' => 'struct.{name, age}', - 'expected' => [new NestedPartialSegment('struct'), new FieldsPartialSegment(['name', 'age'])] + 'expected' => [new NestedPartialSegment('struct'), new FieldsPartialSegment(['name', 'age'])], ]; yield "nested star" => [ 'partials' => 'struct.*', - 'expected' => [new NestedPartialSegment('struct'), new AllPartialSegment()] + 'expected' => [new NestedPartialSegment('struct'), new AllPartialSegment()], ]; } @@ -69,6 +63,6 @@ function invalidPartialsProvider(): Generator yield "nested property on multi-property" => [ 'partials' => '{name, age}.name', - 'expected' => [new FieldsPartialSegment(['name', 'age'])] + 'expected' => [new FieldsPartialSegment(['name', 'age'])], ]; } From fdeddbaa268fe3d50567702a90c4fdddae274945 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 5 Jan 2024 18:09:19 +0100 Subject: [PATCH 045/124] Fix partials --- src/Commands/DataStructuresCacheCommand.php | 10 +- src/Concerns/ResponsableData.php | 2 +- src/Concerns/TransformableData.php | 20 +- .../RequestQueryStringPartialsResolver.php | 6 +- .../TransformedDataCollectionResolver.php | 6 +- src/Resolvers/TransformedDataResolver.php | 24 +- src/Resolvers/VisibleDataFieldsResolver.php | 101 ++--- src/Support/DataClass.php | 54 ++- src/Support/DataStructureProperty.php | 29 ++ src/Support/LazyDataStructureProperty.php | 40 ++ src/Support/Partials/ResolvedPartial.php | 41 +- .../Transformation/TransformationContext.php | 204 +++++++++- .../TransformationContextFactory.php | 104 +++-- tests/DataBenchTest.php | 83 ---- tests/DataTest.php | 27 -- tests/Fakes/MultiLazyData.php | 9 + tests/PartialsTest.php | 385 ++++++++++++++---- .../VisibleDataFieldsResolverTest.php | 74 ++++ .../Support/Caching/CachedDataConfigTest.php | 1 + .../Support/Partials/ResolvedPartialTest.php | 146 +++++++ 20 files changed, 1053 insertions(+), 313 deletions(-) create mode 100644 src/Support/DataStructureProperty.php create mode 100644 src/Support/LazyDataStructureProperty.php delete mode 100644 tests/DataBenchTest.php create mode 100644 tests/Resolvers/VisibleDataFieldsResolverTest.php create mode 100644 tests/Support/Partials/ResolvedPartialTest.php diff --git a/src/Commands/DataStructuresCacheCommand.php b/src/Commands/DataStructuresCacheCommand.php index 6b5f93f3..4e0df8ae 100644 --- a/src/Commands/DataStructuresCacheCommand.php +++ b/src/Commands/DataStructuresCacheCommand.php @@ -30,10 +30,12 @@ public function handle( $progressBar = $this->output->createProgressBar(count($dataClasses)); - foreach ($dataClasses as $dataClass) { - $dataStructureCache->storeDataClass( - DataClass::create(new ReflectionClass($dataClass)) - ); + foreach ($dataClasses as $dataClassString) { + $dataClass = DataClass::create(new ReflectionClass($dataClassString)); + + $dataClass->prepareForCache(); + + $dataStructureCache->storeDataClass($dataClass); $progressBar->advance(); } diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index fd95d501..e65ed6df 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -63,7 +63,7 @@ public function toResponse($request) } return new JsonResponse( - data: $this->transform($contextFactory->get($this)), + data: $this->transform($contextFactory), status: $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK, ); } diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 6dd7fc78..a123f23c 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -15,13 +15,11 @@ trait TransformableData public function transform( null|TransformationContextFactory|TransformationContext $transformationContext = null, ): array { - if ($transformationContext === null) { - $transformationContext = new TransformationContext(); - } - - if ($transformationContext instanceof TransformationContextFactory) { - $transformationContext = $transformationContext->get($this); - } + $transformationContext = match (true) { + $transformationContext instanceof TransformationContext => $transformationContext, + $transformationContext instanceof TransformationContextFactory => $transformationContext->get($this), + $transformationContext === null => new TransformationContext() + }; $resolver = match (true) { $this instanceof BaseDataContract => DataContainer::get()->transformedDataResolver(), @@ -34,25 +32,25 @@ public function transform( /** @var TransformationContext $transformationContext */ if ($dataContext->includePartials->count() > 0) { - $transformationContext->includedPartials->addAll( + $transformationContext->mergeIncludedResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->includePartials) ); } if ($dataContext->excludePartials->count() > 0) { - $transformationContext->excludedPartials->addAll( + $transformationContext->mergeExcludedResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->excludePartials) ); } if ($dataContext->onlyPartials->count() > 0) { - $transformationContext->onlyPartials->addAll( + $transformationContext->mergeOnlyResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->onlyPartials) ); } if ($dataContext->exceptPartials->count() > 0) { - $transformationContext->exceptPartials->addAll( + $transformationContext->mergeExceptResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->exceptPartials) ); } diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php index 29b8576a..5196eab0 100644 --- a/src/Resolvers/RequestQueryStringPartialsResolver.php +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -151,8 +151,10 @@ protected function findField( return $field; } - if (array_key_exists($field, $dataClass->outputMappedProperties)) { - return $dataClass->outputMappedProperties[$field]; + $outputMappedProperties = $dataClass->outputMappedProperties->resolve(); + + if (array_key_exists($field, $outputMappedProperties)) { + return $outputMappedProperties[$field]; } return null; diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index 0bd4dded..3c3de1d6 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -39,8 +39,6 @@ public function execute( ? $context->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) : $context; - // TODO: take into account that a DataCollection, PaginatedDataCollection and CursorPaginatedDataCollection also can have partials - if ($items instanceof DataCollection) { return $this->transformItems($items->items(), $wrap, $context, $nestedContext); } @@ -83,7 +81,7 @@ protected function transformPaginator( TransformationContext $context, TransformationContext $nestedContext, ): array { - $paginator->through(fn (BaseData $data) => $this->transformationClosure($nestedContext)($data)); + $paginator = $paginator->through(fn (BaseData $data) => $this->transformationClosure($nestedContext)($data)); if ($context->transformValues === false) { return $paginator->all(); @@ -111,7 +109,7 @@ protected function transformationClosure( return $data; } - return $data->transform($context); + return $data->transform(clone $context); }; } } diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 11ef908d..11ab3d09 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -108,9 +108,13 @@ protected function resolvePropertyValue( WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled }; - return $value->transform( - $fieldContext->setWrapExecutionType($wrapExecutionType) - ); + $context = clone $fieldContext->setWrapExecutionType($wrapExecutionType); + + $transformed = $value->transform($context); + + $context->rollBackPartialsWhenRequired(); + + return $transformed; } if ( @@ -124,9 +128,13 @@ protected function resolvePropertyValue( WrapExecutionType::TemporarilyDisabled => WrapExecutionType::TemporarilyDisabled }; - return $value->transform( - $fieldContext->setWrapExecutionType($wrapExecutionType) - ); + $context = clone $fieldContext->setWrapExecutionType($wrapExecutionType); + + $transformed = $value->transform($context); + + $context->rollBackPartialsWhenRequired(); + + return $transformed; } if ( @@ -153,7 +161,7 @@ protected function resolvePotentialPartialArray( array $value, TransformationContext $fieldContext, ): array { - if ($fieldContext->exceptPartials->count() > 0) { + if ($fieldContext->exceptPartials && $fieldContext->exceptPartials->count() > 0) { $partials = []; foreach ($fieldContext->exceptPartials as $exceptPartial) { @@ -163,7 +171,7 @@ protected function resolvePotentialPartialArray( return Arr::except($value, $partials); } - if ($fieldContext->onlyPartials->count() > 0) { + if ($fieldContext->onlyPartials && $fieldContext->onlyPartials->count() > 0) { $partials = []; foreach ($fieldContext->onlyPartials as $onlyPartial) { diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index ace1cf91..13a14855 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -11,7 +11,6 @@ use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; use Spatie\LaravelData\Support\Transformation\TransformationContext; -use SplObjectStorage; class VisibleDataFieldsResolver { @@ -27,59 +26,47 @@ public function execute( ): array { $dataInitializedFields = get_object_vars($data); - /** @var array $fields */ - $fields = $dataClass - ->properties - ->reject(function (DataProperty $property) use (&$dataInitializedFields): bool { - if ($property->hidden) { - return true; - } + $fields = $dataClass->transformationFields->resolve(); - if ($property->type->isOptional && ! array_key_exists($property->name, $dataInitializedFields)) { - return true; - } + foreach ($fields as $field => $next) { + if (! array_key_exists($field, $dataInitializedFields)) { + unset($fields[$field]); - return false; - }) - ->map(function (DataProperty $property) use ($transformationContext): null|TransformationContext { - if ( - $property->type->kind->isDataCollectable() - || $property->type->kind->isDataObject() - || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) - ) { - return new TransformationContext( - $transformationContext->transformValues, - $transformationContext->mapPropertyNames, - $transformationContext->wrapExecutionType, - new SplObjectStorage(), - new SplObjectStorage(), - new SplObjectStorage(), - new SplObjectStorage(), - ); - } + continue; + } - return null; - })->all(); + if ($next === true) { + $fields[$field] = new TransformationContext( + $transformationContext->transformValues, + $transformationContext->mapPropertyNames, + $transformationContext->wrapExecutionType, + ); + } + } - $this->performExcept($fields, $transformationContext); + if ($transformationContext->exceptPartials) { + $this->performExcept($fields, $transformationContext); + } if (empty($fields)) { return []; } - $this->performOnly($fields, $transformationContext); + if ($transformationContext->onlyPartials) { + $this->performOnly($fields, $transformationContext); + } - $includedFields = $this->resolveIncludedFields( + $includedFields = $transformationContext->includedPartials ? $this->resolveIncludedFields( $fields, $transformationContext, $dataClass, - ); + ) : []; - $excludedFields = $this->resolveExcludedFields( + $excludedFields = $transformationContext->excludedPartials ? $this->resolveExcludedFields( $fields, $transformationContext, $dataClass, - ); + ) : []; foreach ($fields as $field => $fieldTransFormationContext) { $value = $data->{$field}; @@ -95,7 +82,7 @@ public function execute( } if ($value instanceof RelationalLazy || $value instanceof ConditionalLazy) { - if(! $value->shouldBeIncluded()) { + if (! $value->shouldBeIncluded()) { unset($fields[$field]); } @@ -118,6 +105,9 @@ public function execute( return $fields; } + /** + * @param array $fields + */ protected function performExcept( array &$fields, TransformationContext $transformationContext @@ -136,7 +126,7 @@ protected function performExcept( } if ($nested = $exceptPartial->getNested()) { - $fields[$nested]->exceptPartials->attach($exceptPartial->next()); + $fields[$nested]->addExceptResolvedPartial($exceptPartial->next()); continue; } @@ -151,6 +141,9 @@ protected function performExcept( } } + /** + * @param array $fields + */ protected function performOnly( array &$fields, TransformationContext $transformationContext @@ -166,7 +159,7 @@ protected function performOnly( $onlyFields ??= []; if ($nested = $onlyPartial->getNested()) { - $fields[$nested]->onlyPartials->attach($onlyPartial->next()); + $fields[$nested]->addOnlyResolvedPartial($onlyPartial->next()); $onlyFields[] = $nested; continue; @@ -190,14 +183,16 @@ protected function performOnly( } } + /** + * @param array $fields + */ protected function resolveIncludedFields( - array $fields, + array &$fields, TransformationContext $transformationContext, DataClass $dataClass ): array { $includedFields = []; - foreach ($transformationContext->includedPartials as $includedPartial) { if ($includedPartial->isUndefined()) { continue; @@ -206,15 +201,20 @@ protected function resolveIncludedFields( if ($includedPartial->isAll()) { $includedFields = $dataClass ->properties - ->filter(fn (DataProperty $property) => $property->type->lazyType !== null) + ->filter(fn (DataProperty $property) => $property->type->lazyType !== null && array_key_exists($property->name, $fields)) ->keys() ->all(); + foreach ($includedFields as $includedField) { + // can be null when field is a non data object/collectable or array + $fields[$includedField]?->addIncludedResolvedPartial($includedPartial->next()); + } + break; } if ($nested = $includedPartial->getNested()) { - $fields[$nested]->includedPartials->attach($includedPartial->next()); + $fields[$nested]->addIncludedResolvedPartial($includedPartial->next()); $includedFields[] = $nested; continue; @@ -228,8 +228,11 @@ protected function resolveIncludedFields( return $includedFields; } + /** + * @param array $fields + */ protected function resolveExcludedFields( - array $fields, + array &$fields, TransformationContext $transformationContext, DataClass $dataClass ): array { @@ -243,15 +246,19 @@ protected function resolveExcludedFields( if ($excludedPartial->isAll()) { $excludedFields = $dataClass ->properties - ->filter(fn (DataProperty $property) => $property->type->lazyType !== null) + ->filter(fn (DataProperty $property) => $property->type->lazyType !== null && array_key_exists($property->name, $fields)) ->keys() ->all(); + foreach ($excludedFields as $excludedField) { + $fields[$excludedField]?->addExcludedResolvedPartial($excludedPartial->next()); + } + break; } if ($nested = $excludedPartial->getNested()) { - $fields[$nested]->excludedPartials->attach($excludedPartial->next()); + $fields[$nested]->addExcludedResolvedPartial($excludedPartial->next()); $excludedFields[] = $nested; continue; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 9f02a042..dc4f09d3 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Contracts\ValidateableData; use Spatie\LaravelData\Contracts\WrappableData; +use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Mappers\ProvidedNameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; @@ -50,7 +51,8 @@ public function __construct( public readonly ?array $allowedRequestExcludes, public readonly ?array $allowedRequestOnly, public readonly ?array $allowedRequestExcept, - public readonly array $outputMappedProperties, + public DataStructureProperty $outputMappedProperties, + public DataStructureProperty $transformationFields ) { } @@ -83,11 +85,13 @@ public static function create(ReflectionClass $class): self $responsable = $class->implementsInterface(ResponsableData::class); - $outputMappedProperties = $properties - ->map(fn (DataProperty $property) => $property->outputMappedName) - ->filter() - ->flip() - ->toArray(); + $outputMappedProperties = new LazyDataStructureProperty( + fn () => $properties + ->map(fn (DataProperty $property) => $property->outputMappedName) + ->filter() + ->flip() + ->toArray() + ); return new self( name: $class->name, @@ -110,6 +114,7 @@ public static function create(ReflectionClass $class): self allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, outputMappedProperties: $outputMappedProperties, + transformationFields: static::resolveTransformationFields($properties), ); } @@ -183,4 +188,41 @@ protected static function resolveDefaultValues( $values ); } + + /** + * @param Collection $properties + * + * @return LazyDataStructureProperty> + */ + protected static function resolveTransformationFields( + Collection $properties, + ): LazyDataStructureProperty { + $closure = fn () => $properties + ->reject(fn (DataProperty $property): bool => $property->hidden) + ->map(function (DataProperty $property): null|true { + if ( + $property->type->kind->isDataCollectable() + || $property->type->kind->isDataObject() + || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) + ) { + return true; + } + + return null; + }) + ->all(); + + return new LazyDataStructureProperty($closure); + } + + public function prepareForCache(): void + { + if($this->outputMappedProperties instanceof LazyDataStructureProperty) { + $this->outputMappedProperties = $this->outputMappedProperties->toDataStructureProperty(); + } + + if($this->transformationFields instanceof LazyDataStructureProperty) { + $this->transformationFields = $this->transformationFields->toDataStructureProperty(); + } + } } diff --git a/src/Support/DataStructureProperty.php b/src/Support/DataStructureProperty.php new file mode 100644 index 00000000..400698a0 --- /dev/null +++ b/src/Support/DataStructureProperty.php @@ -0,0 +1,29 @@ +resolved; + } + + /** + * @param T $resolved + */ + public function setResolved(mixed $resolved): void + { + $this->resolved = $resolved; + } +} diff --git a/src/Support/LazyDataStructureProperty.php b/src/Support/LazyDataStructureProperty.php new file mode 100644 index 00000000..ab12f90a --- /dev/null +++ b/src/Support/LazyDataStructureProperty.php @@ -0,0 +1,40 @@ +resolved)) { + $this->resolved = ($this->value)(); + } + + return $this->resolved; + } + + public function toDataStructureProperty(): DataStructureProperty + { + $property = new DataStructureProperty(); + + $property->setResolved($this->resolve()); + + return $property; + } +} diff --git a/src/Support/Partials/ResolvedPartial.php b/src/Support/Partials/ResolvedPartial.php index 5d837861..05c7386f 100644 --- a/src/Support/Partials/ResolvedPartial.php +++ b/src/Support/Partials/ResolvedPartial.php @@ -12,6 +12,8 @@ class ResolvedPartial implements Stringable { protected int $segmentCount; + protected bool $endsInAll; + /** * @param array $segments * @param int $pointer @@ -21,22 +23,29 @@ public function __construct( public int $pointer = 0, ) { $this->segmentCount = count($segments); + $this->endsInAll = $this->segmentCount === 0 + ? false + : $this->segments[$this->segmentCount - 1] instanceof AllPartialSegment; } - public function isUndefined() + public function isUndefined(): bool { - return $this->pointer === $this->segmentCount; + return ! $this->endsInAll && $this->pointer >= $this->segmentCount; } - public function isAll() + public function isAll(): bool { - return $this->getCurrentSegment() instanceof AllPartialSegment; + return $this->endsInAll && $this->pointer >= $this->segmentCount - 1; } public function getNested(): ?string { $segment = $this->getCurrentSegment(); + if ($segment === null) { + return null; + } + if (! $segment instanceof NestedPartialSegment) { return null; } @@ -46,8 +55,16 @@ public function getNested(): ?string public function getFields(): ?array { + if ($this->isUndefined()) { + return null; + } + $segment = $this->getCurrentSegment(); + if ($segment === null) { + return null; + } + if (! $segment instanceof FieldsPartialSegment) { return null; } @@ -79,7 +96,7 @@ public function toLaravel(): array if ($segment instanceof FieldsPartialSegment) { $segmentsAsString = count($segments) === 0 ? '' - : implode('.', $segments) . '.'; + : implode('.', $segments).'.'; return array_map( fn (string $field) => "{$segmentsAsString}{$field}", @@ -93,20 +110,14 @@ public function toLaravel(): array public function next(): self { - if ($this->isUndefined() || $this->isAll()) { - return $this; - } - $this->pointer++; return $this; } - public function back(): self + public function rollbackWhenRequired(): void { $this->pointer--; - - return $this; } public function reset(): self @@ -116,13 +127,13 @@ public function reset(): self return $this; } - public function getCurrentSegment(): PartialSegment + protected function getCurrentSegment(): ?PartialSegment { - return $this->segments[$this->pointer]; + return $this->segments[$this->pointer] ?? null; } public function __toString(): string { - return implode('.', $this->segments) . " (current: {$this->pointer})"; + return implode('.', $this->segments)." (current: {$this->pointer})"; } } diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 1808a5a6..9312b816 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -5,28 +5,31 @@ use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use SplObjectStorage; +use Stringable; -class TransformationContext +class TransformationContext implements Stringable { /** - * @param SplObjectStorage $includedPartials - * @param SplObjectStorage $excludedPartials - * @param SplObjectStorage $onlyPartials - * @param SplObjectStorage $exceptPartials + * @param null|SplObjectStorage $includedPartials for internal use only + * @param null|SplObjectStorage $excludedPartials for internal use only + * @param null|SplObjectStorage $onlyPartials for internal use only + * @param null|SplObjectStorage $exceptPartials for internal use only */ public function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public SplObjectStorage $includedPartials = new SplObjectStorage(), - public SplObjectStorage $excludedPartials = new SplObjectStorage(), - public SplObjectStorage $onlyPartials = new SplObjectStorage(), - public SplObjectStorage $exceptPartials = new SplObjectStorage(), + public ?SplObjectStorage $includedPartials = null, + public ?SplObjectStorage $excludedPartials = null, + public ?SplObjectStorage $onlyPartials = null, + public ?SplObjectStorage $exceptPartials = null, ) { } public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self { + // Todo: remove and run directly on object + return new self( $this->transformValues, $this->mapPropertyNames, @@ -37,4 +40,187 @@ public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self $this->exceptPartials, ); } + + public function addIncludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void + { + if ($this->includedPartials === null) { + $this->includedPartials = new SplObjectStorage(); + } + + foreach ($resolvedPartials as $resolvedPartial) { + $this->includedPartials->attach($resolvedPartial); + } + } + + public function addExcludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void + { + if ($this->excludedPartials === null) { + $this->excludedPartials = new SplObjectStorage(); + } + + foreach ($resolvedPartials as $resolvedPartial) { + $this->excludedPartials->attach($resolvedPartial); + } + } + + public function addOnlyResolvedPartial(ResolvedPartial ...$resolvedPartials): void + { + if ($this->onlyPartials === null) { + $this->onlyPartials = new SplObjectStorage(); + } + + foreach ($resolvedPartials as $resolvedPartial) { + $this->onlyPartials->attach($resolvedPartial); + } + } + + public function addExceptResolvedPartial(ResolvedPartial ...$resolvedPartials): void + { + if ($this->exceptPartials === null) { + $this->exceptPartials = new SplObjectStorage(); + } + + foreach ($resolvedPartials as $resolvedPartial) { + $this->exceptPartials->attach($resolvedPartial); + } + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeIncludedResolvedPartials(SplObjectStorage $partials): void + { + if ($this->includedPartials === null) { + $this->includedPartials = new SplObjectStorage(); + } + + $this->includedPartials->addAll($partials); + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeExcludedResolvedPartials(SplObjectStorage $partials): void + { + if ($this->excludedPartials === null) { + $this->excludedPartials = new SplObjectStorage(); + } + + $this->excludedPartials->addAll($partials); + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeOnlyResolvedPartials(SplObjectStorage $partials): void + { + if ($this->onlyPartials === null) { + $this->onlyPartials = new SplObjectStorage(); + } + + $this->onlyPartials->addAll($partials); + } + + /** + * @param SplObjectStorage $partials + */ + public function mergeExceptResolvedPartials(SplObjectStorage $partials): void + { + if ($this->exceptPartials === null) { + $this->exceptPartials = new SplObjectStorage(); + } + + $this->exceptPartials->addAll($partials); + } + + public function rollBackPartialsWhenRequired(): void + { + if ($this->includedPartials !== null) { + foreach ($this->includedPartials as $includedPartial) { + $includedPartial->rollbackWhenRequired(); + } + } + + if ($this->excludedPartials !== null) { + foreach ($this->excludedPartials as $excludedPartial) { + $excludedPartial->rollbackWhenRequired(); + } + } + + if ($this->onlyPartials !== null) { + foreach ($this->onlyPartials as $onlyPartial) { + $onlyPartial->rollbackWhenRequired(); + } + } + + if ($this->exceptPartials !== null) { + foreach ($this->exceptPartials as $exceptPartial) { + $exceptPartial->rollbackWhenRequired(); + } + } + } + + public function __clone(): void + { + if ($this->includedPartials !== null) { + $this->includedPartials = clone $this->includedPartials; + } + + if ($this->excludedPartials !== null) { + $this->excludedPartials = clone $this->excludedPartials; + } + + if ($this->onlyPartials !== null) { + $this->onlyPartials = clone $this->onlyPartials; + } + + if ($this->exceptPartials !== null) { + $this->exceptPartials = clone $this->exceptPartials; + } + } + + public function __toString(): string + { + $output = 'Transformation Context '.spl_object_id($this).PHP_EOL; + + $output .= "- wrapExecutionType: {$this->wrapExecutionType->name}".PHP_EOL; + + if ($this->transformValues) { + $output .= "- transformValues: true".PHP_EOL; + } + + if ($this->mapPropertyNames) { + $output .= "- mapPropertyNames: true".PHP_EOL; + } + + if ($this->includedPartials !== null && $this->includedPartials->count() > 0) { + $output .= "- includedPartials:".PHP_EOL; + foreach ($this->includedPartials as $includedPartial) { + $output .= " - {$includedPartial}".PHP_EOL; + } + } + + if ($this->excludedPartials !== null && $this->excludedPartials->count() > 0) { + $output .= "- excludedPartials:".PHP_EOL; + foreach ($this->excludedPartials as $excludedPartial) { + $output .= " - {$excludedPartial}".PHP_EOL; + } + } + + if ($this->onlyPartials !== null && $this->onlyPartials->count() > 0) { + $output .= "- onlyPartials:".PHP_EOL; + foreach ($this->onlyPartials as $onlyPartial) { + $output .= " - {$onlyPartial}".PHP_EOL; + } + } + + if ($this->exceptPartials !== null && $this->exceptPartials->count() > 0) { + $output .= "- exceptPartials:".PHP_EOL; + foreach ($this->exceptPartials as $exceptPartial) { + $output .= " - {$exceptPartial}".PHP_EOL; + } + } + + return $output; + } } diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 98a9a1f3..bf3d16cf 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -19,62 +19,78 @@ public static function create(): self } /** - * @param SplObjectStorage $includedPartials - * @param SplObjectStorage $excludedPartials - * @param SplObjectStorage $onlyPartials - * @param SplObjectStorage $exceptPartials + * @param ?SplObjectStorage $includedPartials + * @param ?SplObjectStorage $excludedPartials + * @param ?SplObjectStorage $onlyPartials + * @param ?SplObjectStorage $exceptPartials */ protected function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public SplObjectStorage $includedPartials = new SplObjectStorage(), - public SplObjectStorage $excludedPartials = new SplObjectStorage(), - public SplObjectStorage $onlyPartials = new SplObjectStorage(), - public SplObjectStorage $exceptPartials = new SplObjectStorage(), + public ?SplObjectStorage $includedPartials = null, + public ?SplObjectStorage $excludedPartials = null, + public ?SplObjectStorage $onlyPartials = null, + public ?SplObjectStorage $exceptPartials = null, ) { } public function get( BaseData|BaseDataCollectable $data, ): TransformationContext { - $includedPartials = new SplObjectStorage(); + $includedPartials = null; - foreach ($this->includedPartials as $include) { - $resolved = $include->resolve($data); + if ($this->includedPartials) { + $includedPartials = new SplObjectStorage(); - if($resolved) { - $includedPartials->attach($resolved); + foreach ($this->includedPartials as $include) { + $resolved = $include->resolve($data); + + if ($resolved) { + $includedPartials->attach($resolved); + } } } - $excludedPartials = new SplObjectStorage(); + $excludedPartials = null; + + if ($this->excludedPartials) { + $excludedPartials = new SplObjectStorage(); - foreach ($this->excludedPartials as $exclude) { - $resolved = $exclude->resolve($data); + foreach ($this->excludedPartials as $exclude) { + $resolved = $exclude->resolve($data); - if($resolved) { - $excludedPartials->attach($resolved); + if ($resolved) { + $excludedPartials->attach($resolved); + } } } - $onlyPartials = new SplObjectStorage(); + $onlyPartials = null; + + if ($this->onlyPartials) { + $onlyPartials = new SplObjectStorage(); - foreach ($this->onlyPartials as $only) { - $resolved = $only->resolve($data); + foreach ($this->onlyPartials as $only) { + $resolved = $only->resolve($data); - if($resolved) { - $onlyPartials->attach($resolved); + if ($resolved) { + $onlyPartials->attach($resolved); + } } } - $exceptPartials = new SplObjectStorage(); + $exceptPartials = null; - foreach ($this->exceptPartials as $except) { - $resolved = $except->resolve($data); + if ($this->exceptPartials) { + $exceptPartials = new SplObjectStorage(); - if($resolved) { - $exceptPartials->attach($resolved); + foreach ($this->exceptPartials as $except) { + $resolved = $except->resolve($data); + + if ($resolved) { + $exceptPartials->attach($resolved); + } } } @@ -112,6 +128,10 @@ public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static public function addIncludePartial(Partial ...$partial): static { + if ($this->includedPartials === null) { + $this->includedPartials = new SplObjectStorage(); + } + foreach ($partial as $include) { $this->includedPartials->attach($include); } @@ -121,6 +141,10 @@ public function addIncludePartial(Partial ...$partial): static public function addExcludePartial(Partial ...$partial): static { + if ($this->excludedPartials === null) { + $this->excludedPartials = new SplObjectStorage(); + } + foreach ($partial as $exclude) { $this->excludedPartials->attach($exclude); } @@ -130,6 +154,10 @@ public function addExcludePartial(Partial ...$partial): static public function addOnlyPartial(Partial ...$partial): static { + if ($this->onlyPartials === null) { + $this->onlyPartials = new SplObjectStorage(); + } + foreach ($partial as $only) { $this->onlyPartials->attach($only); } @@ -139,6 +167,10 @@ public function addOnlyPartial(Partial ...$partial): static public function addExceptPartial(Partial ...$partial): static { + if ($this->exceptPartials === null) { + $this->exceptPartials = new SplObjectStorage(); + } + foreach ($partial as $except) { $this->exceptPartials->attach($except); } @@ -151,6 +183,10 @@ public function addExceptPartial(Partial ...$partial): static */ public function mergeIncludePartials(SplObjectStorage $partials): static { + if ($this->includedPartials === null) { + $this->includedPartials = new SplObjectStorage(); + } + $this->includedPartials->addAll($partials); return $this; @@ -161,6 +197,10 @@ public function mergeIncludePartials(SplObjectStorage $partials): static */ public function mergeExcludePartials(SplObjectStorage $partials): static { + if ($this->excludedPartials === null) { + $this->excludedPartials = new SplObjectStorage(); + } + $this->excludedPartials->addAll($partials); return $this; @@ -171,6 +211,10 @@ public function mergeExcludePartials(SplObjectStorage $partials): static */ public function mergeOnlyPartials(SplObjectStorage $partials): static { + if ($this->onlyPartials === null) { + $this->onlyPartials = new SplObjectStorage(); + } + $this->onlyPartials->addAll($partials); return $this; @@ -181,6 +225,10 @@ public function mergeOnlyPartials(SplObjectStorage $partials): static */ public function mergeExceptPartials(SplObjectStorage $partials): static { + if ($this->exceptPartials === null) { + $this->exceptPartials = new SplObjectStorage(); + } + $this->exceptPartials->addAll($partials); return $this; diff --git a/tests/DataBenchTest.php b/tests/DataBenchTest.php deleted file mode 100644 index 95badbe5..00000000 --- a/tests/DataBenchTest.php +++ /dev/null @@ -1,83 +0,0 @@ -getDataClass(ComplicatedData::class); - app(DataConfig::class)->getDataClass(SimpleData::class); - app(DataConfig::class)->getDataClass(NestedData::class); - - $collection = Collection::times(1000, fn () => clone $data); - - $dataCollection = ComplicatedData::collect($collection, DataCollection::class); - - bench( - fn () => $dataCollection->toArray(), - fn () => $dataCollection->toCollection()->map(fn (ComplicatedData $data) => $data->toUserDefinedToArray()), - name: 'collection', - times: 10 - ); - - bench( - fn () => $data->toArray(), - fn () => $data->toUserDefinedToArray(), - name: 'single', - ); -})->skip('Use PHPBench for now'); - -function benchSingle(Closure $closure, $times): float -{ - $start = microtime(true); - - for ($i = 0; $i < $times; $i++) { - $closure(); - } - - $end = microtime(true); - - return ($end - $start) / $times; -} - -function bench(Closure $data, Closure $userDefined, string $name, $times = 100): void -{ - $dataBench = benchSingle($data, $times); - $userDefinedBench = benchSingle($userDefined, $times); - - dump("{$name} data - " . number_format($dataBench, 10)); - dump("{$name} user defined - " . number_format($userDefinedBench, 10)); - dump("{$name} data is " . round($dataBench / $userDefinedBench, 0) . " times slower than user defined"); - -} diff --git a/tests/DataTest.php b/tests/DataTest.php index 66a8b1e1..96169a45 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1394,33 +1394,6 @@ public function __construct( expect($invaded->_dataContext)->toBeNull(); }); -// TODO: extend tests here -it('can use an array to store data', function () { - $dataClass = new class ( - [LazyData::from('A'), LazyData::from('B')], - collect([LazyData::from('A'), LazyData::from('B')]), - ) extends Data { - public function __construct( - #[DataCollectionOf(SimpleData::class)] - public array $array, - #[DataCollectionOf(SimpleData::class)] - public Collection $collection, - ) { - } - }; - - expect($dataClass->include('array.name', 'collection.name')->toArray())->toBe([ - 'array' => [ - ['name' => 'A'], - ['name' => 'B'], - ], - 'collection' => [ - ['name' => 'A'], - ['name' => 'B'], - ], - ]); -}); - it('can set a default value for data object', function () { $dataObject = new class ('', '') extends Data { diff --git a/tests/Fakes/MultiLazyData.php b/tests/Fakes/MultiLazyData.php index 3f3cf898..a892246a 100644 --- a/tests/Fakes/MultiLazyData.php +++ b/tests/Fakes/MultiLazyData.php @@ -14,6 +14,15 @@ public function __construct( ) { } + public static function fromMultiple(string $artist, string $name, int $year): static + { + return new self( + Lazy::create(fn () => $artist), + Lazy::create(fn () => $name), + Lazy::create(fn () => $year), + ); + } + public static function fromDto(DummyDto $dto) { return new self( diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index a9d61af3..02cc17e7 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -879,20 +879,6 @@ public function __construct( ]); }); -test('except has precedence over only', function () { - $data = new MultiData('Hello', 'World'); - - expect( - (clone $data)->onlyWhen('first', true) - ->exceptWhen('first', true) - ->toArray() - )->toMatchArray(['second' => 'World']); - - expect( - (clone $data)->exceptWhen('first', true)->onlyWhen('first', true)->toArray() - )->toMatchArray(['second' => 'World']); -})->skip('We know first perform except and then only, test can be removed'); - it('can perform only and except on array properties', function () { $data = new class ('Hello World', ['string' => 'Hello World', 'int' => 42]) extends Data { public function __construct( @@ -1087,37 +1073,76 @@ public function __construct( ]); }); -it('can work with the different types of lazy data collections', function ( - Data $dataClass, - Closure $itemsClosure -) { - $data = $dataClass::from([ - 'lazyCollection' => $itemsClosure([ - SimpleData::from('A'), - SimpleData::from('B'), - ]), +it('can work with lazy array data collections', function () { + $dataClass = new class () extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|array $lazyCollection; - 'nestedLazyCollection' => $itemsClosure([ - NestedLazyData::from('C'), - NestedLazyData::from('D'), - ]), + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|array $nestedLazyCollection; + }; + + $dataClass->lazyCollection = Lazy::create(fn () => [ + SimpleData::from('A'), + SimpleData::from('B'), ]); - expect($data->toArray())->toMatchArray([]); + $dataClass->nestedLazyCollection = Lazy::create(fn () => [ + NestedLazyData::from('C'), + NestedLazyData::from('D'), + ]); + + expect($dataClass->toArray())->toMatchArray([]); + + expect($dataClass->include('lazyCollection')->toArray())->toMatchArray([ + 'lazyCollection' => [ + ['string' => 'A'], + ['string' => 'B'], + ], + ]); - expect($data->include('lazyCollection')->toArray())->toMatchArray([ + expect($dataClass->include('lazyCollection', 'nestedLazyCollection.simple')->toArray())->toMatchArray([ 'lazyCollection' => [ ['string' => 'A'], ['string' => 'B'], ], 'nestedLazyCollection' => [ - [], - [], + ['simple' => ['string' => 'C']], + ['simple' => ['string' => 'D']], ], ]); +}); - expect($data->include('lazyCollection', 'nestedLazyCollection.simple')->toArray())->toMatchArray([ +it('can work with lazy laravel data collections', function () { + $dataClass = new class () extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|Collection $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|Collection $nestedLazyCollection; + }; + + $dataClass->lazyCollection = Lazy::create(fn () => collect([ + SimpleData::from('A'), + SimpleData::from('B'), + ])); + + $dataClass->nestedLazyCollection = Lazy::create(fn () => collect([ + NestedLazyData::from('C'), + NestedLazyData::from('D'), + ])); + + expect($dataClass->toArray())->toMatchArray([]); + + expect($dataClass->include('lazyCollection')->toArray())->toMatchArray([ + 'lazyCollection' => [ + ['string' => 'A'], + ['string' => 'B'], + ], + ]); + + expect($dataClass->include('lazyCollection', 'nestedLazyCollection.simple')->toArray())->toMatchArray([ 'lazyCollection' => [ ['string' => 'A'], ['string' => 'B'], @@ -1128,40 +1153,49 @@ public function __construct( ['simple' => ['string' => 'D']], ], ]); -})->with(function () { - yield 'array' => [ - fn () => new class () extends Data { - #[DataCollectionOf(SimpleData::class)] - public Lazy|array $lazyCollection; +}); - #[DataCollectionOf(NestedLazyData::class)] - public Lazy|array $nestedLazyCollection; - }, - fn () => fn (array $items) => $items, - ]; +it('can work with lazy laravel data paginators', function () { + $dataClass = new class () extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|Collection $lazyCollection; - yield 'collection' => [ - fn () => new class () extends Data { - #[DataCollectionOf(SimpleData::class)] - public Lazy|Collection $lazyCollection; + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|Collection $nestedLazyCollection; + }; - #[DataCollectionOf(NestedLazyData::class)] - public Lazy|Collection $nestedLazyCollection; - }, - fn () => fn (array $items) => $items, - ]; + $dataClass->lazyCollection = Lazy::create(fn () => new LengthAwarePaginator([ + SimpleData::from('A'), + SimpleData::from('B'), + ], total: 15, perPage: 15)); - yield 'paginator' => [ - fn () => new class () extends Data { - #[DataCollectionOf(SimpleData::class)] - public Lazy|LengthAwarePaginator $lazyCollection; + $dataClass->nestedLazyCollection = Lazy::create(fn () => new LengthAwarePaginator([ + NestedLazyData::from('C'), + NestedLazyData::from('D'), + ], total: 15, perPage: 15)); + + expect($dataClass->toArray())->toMatchArray([]); - #[DataCollectionOf(NestedLazyData::class)] - public Lazy|LengthAwarePaginator $nestedLazyCollection; - }, - fn () => fn (array $items) => new LengthAwarePaginator($items, count($items), 15), - ]; -})->skip('Impelemnt further'); + + $array = $dataClass->include('lazyCollection')->toArray(); + + expect($array['lazyCollection']['data'])->toMatchArray([ + ['string' => 'A'], + ['string' => 'B'], + ]); + expect($array)->not()->toHaveKey('nestedLazyCollection'); + + $array = $dataClass->include('lazyCollection', 'nestedLazyCollection.simple')->toArray(); + + expect($array['lazyCollection']['data'])->toMatchArray([ + ['string' => 'A'], + ['string' => 'B'], + ]); + expect($array['nestedLazyCollection']['data'])->toMatchArray([ + ['simple' => ['string' => 'C']], + ['simple' => ['string' => 'D']], + ]); +}); it('partials are always reset when transforming again', function () { $dataClass = new class (Lazy::create(fn () => NestedLazyData::from('Hello World'))) extends Data { @@ -1171,11 +1205,6 @@ public function __construct( } }; - dd($dataClass->include('nested')->exclude()->toArray()); - // ['nested' => ['simple' => ['string' => 'Hello World']],] - $dataClass->toArray(); - // ['nested' => ['simple' => ['string' => 'Hello World']],] - expect($dataClass->include('nested.simple')->toArray())->toBe([ 'nested' => ['simple' => ['string' => 'Hello World']], ]); @@ -1185,8 +1214,228 @@ public function __construct( ]); expect($dataClass->include()->toArray())->toBeEmpty(); -})->skip('Add a reset partials method'); +}); + +it('can define permanent partials which will always be used', function () { + $dataClass = new class( + Lazy::create(fn () => NestedLazyData::from('Hello World')), + Lazy::create(fn () => 'Hello World'), + ) extends Data { + public function __construct( + public Lazy|NestedLazyData $nested, + public Lazy|string $string, + ) { + } + + protected function includeProperties(): array + { + return [ + 'nested.simple', + ]; + } + }; + + expect($dataClass->toArray())->toBe([ + 'nested' => ['simple' => ['string' => 'Hello World']], + ]); + + expect($dataClass->include('string')->toArray())->toBe([ + 'nested' => ['simple' => ['string' => 'Hello World']], + 'string' => 'Hello World', + ]); + + expect($dataClass->toArray())->toBe([ + 'nested' => ['simple' => ['string' => 'Hello World']], + ]); +}); + +it('can combine multiple partials', function ( + array $include, + array $exclude, + array $only, + array $except, + array $expected +) { + $dataClass = new class( + Lazy::create(fn () => NestedLazyData::from('Hello World')), + Lazy::create(fn () => NestedLazyData::collect(['Hello', 'World'])), + Lazy::create(fn () => SimpleData::from('Hello World')), + Lazy::create(fn () => MultiLazyData::from('Hello', 'World', 42)), + Lazy::create(fn () => 'Hello World')->defaultIncluded(), + ) extends Data { + public function __construct( + public Lazy|NestedLazyData $nested, + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|array $collection, + public Lazy|SimpleData $simple, + public Lazy|MultiLazyData $multi, + public Lazy|string $string, + ) { + } + }; + + $array = $dataClass->include(...$include)->exclude(...$exclude)->only(...$only)->except(...$except)->toArray(); + + expect($array)->toMatchArray($expected); +})->with(function () { + yield 'no includes' => [ + 'include' => [], + 'exclude' => [], + 'only' => [], + 'except' => [], + 'expected' => [ + 'string' => 'Hello World', + ], + ]; + + yield 'include and exclude' => [ + 'include' => ['simple'], + 'exclude' => ['string'], + 'only' => [], + 'except' => [], + 'expected' => [ + 'simple' => ['string' => 'Hello World'], + ], + ]; + + yield 'combined include' => [ + 'include' => ['multi.*', 'simple', 'collection.*'], + 'exclude' => [], + 'only' => [], + 'except' => [], + 'expected' => [ + 'collection' => [ + ['simple' => ['string' => 'Hello']], + ['simple' => ['string' => 'World']], + ], + 'simple' => ['string' => 'Hello World'], + 'multi' => [ + 'artist' => 'Hello', + 'name' => 'World', + 'year' => 42, + ], + 'string' => 'Hello World', + ], + ]; + + yield 'included similar paths' => [ + 'include' => ['multi.artist', 'multi.name'], + 'exclude' => [], + 'only' => [], + 'except' => [], + 'expected' => [ + 'multi' => [ + 'artist' => 'Hello', + 'name' => 'World', + ], + 'string' => 'Hello World', + ], + ]; + + yield 'include all' => [ + 'include' => ['*'], + 'exclude' => [], + 'only' => [], + 'except' => [], + 'expected' => [ + 'collection' => [ + ['simple' => ['string' => 'Hello']], + ['simple' => ['string' => 'World']], + ], + 'simple' => ['string' => 'Hello World'], + 'multi' => [ + 'artist' => 'Hello', + 'name' => 'World', + 'year' => 42, + ], + 'string' => 'Hello World', + ], + ]; + + yield 'except and only' => [ + 'include' => ['multi.*'], + 'exclude' => [], + 'only' => ['multi.{artist,name}'], + 'except' => ['multi.year'], + 'expected' => [ + 'multi' => [ + 'artist' => 'Hello', + 'name' => 'World', + ], + ], + ]; +}); it('can set partials on a nested data object and these will be respected', function () { + class TestMultiLazyNestedDataWithObjectAndCollection extends Data + { + public function __construct( + public Lazy|NestedLazyData $nested, + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|array $nestedCollection, + ) { + } + } -})->skip('Impelemnt'); + $collection = new DataCollection(\TestMultiLazyNestedDataWithObjectAndCollection::class, [ + new \TestMultiLazyNestedDataWithObjectAndCollection( + NestedLazyData::from('A'), + [ + NestedLazyData::from('B1')->include('simple'), + NestedLazyData::from('B2'), + ], + ), + new \TestMultiLazyNestedDataWithObjectAndCollection( + NestedLazyData::from('C'), + [ + NestedLazyData::from('D1'), + NestedLazyData::from('D2')->include('simple.string'), + ], + ), + ]); + + $collection->include('nested.simple'); + + $data = new class(Lazy::create(fn () => $collection)) extends Data { + public function __construct( + #[DataCollectionOf(\TestMultiLazyNestedDataWithObjectAndCollection::class)] + public Lazy|DataCollection $collection + ) { + } + }; + + expect($data->include('collection')->toArray())->toMatchArray([ + 'collection' => [ + [ + 'nested' => [ + 'simple' => [ + 'string' => 'A', + ], + ], + 'nestedCollection' => [ + [ + 'simple' => [ + 'string' => 'B1', + ], + ], + [], + ], + ], + [ + 'nested' => [ + 'simple' => [ + 'string' => 'C', + ], + ], + 'nestedCollection' => [ + [], + [ + 'simple' => [ + 'string' => 'D2', + ], + ], + ], + ], + ], + ]); +}); diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php new file mode 100644 index 00000000..2f508e12 --- /dev/null +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -0,0 +1,74 @@ +execute( + $data, + app(DataConfig::class)->getDataClass($data::class), + $contextFactory->get($data) + ); +} + +it('will hide hidden fields', function () { + $dataClass = new class() extends Data { + public string $visible = 'visible'; + + #[Hidden] + public string $hidden = 'hidden'; + }; + + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + 'visible' => null, + ]); +}); + +it('will hide optional fields which are unitialized', function () { + $dataClass = new class() extends Data { + public string $visible = 'visible'; + + public Optional|string $optional; + }; + + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + 'visible' => null, + ]); +}); + +// TODO write tests + +it('can perform an excepts', function () { +// $dataClass = new class() extends Data { +// public function __construct( +// public string $visible = 'visible', +// public SimpleData $simple = new SimpleData('simple'), +// public NestedData $nestedData = new NestedData(new SimpleData('simple')), +// public array $collection = +// ) { +// } +// }; +// +// expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ +// 'multi' => new TransformationContext( +// transformValues: true, +// mapPropertyNames: true, +// wrapExecutionType: true, +// new SplObjectStorage(), +// new SplObjectStorage(), +// new SplObjectStorage(), +// new SplObjectStorage(), +// ), +// ]); +}); diff --git a/tests/Support/Caching/CachedDataConfigTest.php b/tests/Support/Caching/CachedDataConfigTest.php index 6f0fc621..87f5b2b6 100644 --- a/tests/Support/Caching/CachedDataConfigTest.php +++ b/tests/Support/Caching/CachedDataConfigTest.php @@ -42,6 +42,7 @@ it('will load cached data classes', function () { $dataClass = DataClass::create(new ReflectionClass(SimpleData::class)); + $dataClass->prepareForCache(); $mock = Mockery::mock( new DataStructureCache(config('data.structure_caching.cache')), diff --git a/tests/Support/Partials/ResolvedPartialTest.php b/tests/Support/Partials/ResolvedPartialTest.php new file mode 100644 index 00000000..3f5acf95 --- /dev/null +++ b/tests/Support/Partials/ResolvedPartialTest.php @@ -0,0 +1,146 @@ +isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(['name', 'age']); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + $partial->next(); // level 2 - non existing + + expect($partial->isUndefined())->toBeTrue(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(['name', 'age']); + + $partial->next(); // level 2 - non existing + $partial->next(); // level 3 - non existing + + expect($partial->isUndefined())->toBeTrue(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 2 - non existing + + expect($partial->isUndefined())->toBeTrue(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(['name', 'age']); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); +}); + +it('can use the pointer system when ending in an all', function (){ + $partial = new ResolvedPartial([ + new NestedPartialSegment('struct'), + new AllPartialSegment(), + ]); + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + $partial->next(); // level 2 - non existing + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 2 - non existing + $partial->next(); // level 3 - non existing + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 2 - non existing + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); +}); From aa48d7dd26f215da055dadfb7255b15725c683c1 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 5 Jan 2024 17:09:46 +0000 Subject: [PATCH 046/124] Fix styling --- src/Resolvers/VisibleDataFieldsResolver.php | 1 - tests/DataTest.php | 1 - tests/PartialsTest.php | 6 +-- .../VisibleDataFieldsResolverTest.php | 47 +++++++++---------- .../Support/Partials/ResolvedPartialTest.php | 2 +- 5 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index 13a14855..6a6eba63 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Resolvers; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataClass; diff --git a/tests/DataTest.php b/tests/DataTest.php index 96169a45..cf9f7fcc 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -43,7 +43,6 @@ use Spatie\LaravelData\Tests\Fakes\DataWithMapper; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; -use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; use Spatie\LaravelData\Tests\Fakes\MultiData; use Spatie\LaravelData\Tests\Fakes\MultiNestedData; diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 02cc17e7..f4423b46 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -1217,7 +1217,7 @@ public function __construct( }); it('can define permanent partials which will always be used', function () { - $dataClass = new class( + $dataClass = new class ( Lazy::create(fn () => NestedLazyData::from('Hello World')), Lazy::create(fn () => 'Hello World'), ) extends Data { @@ -1256,7 +1256,7 @@ protected function includeProperties(): array array $except, array $expected ) { - $dataClass = new class( + $dataClass = new class ( Lazy::create(fn () => NestedLazyData::from('Hello World')), Lazy::create(fn () => NestedLazyData::collect(['Hello', 'World'])), Lazy::create(fn () => SimpleData::from('Hello World')), @@ -1396,7 +1396,7 @@ public function __construct( $collection->include('nested.simple'); - $data = new class(Lazy::create(fn () => $collection)) extends Data { + $data = new class (Lazy::create(fn () => $collection)) extends Data { public function __construct( #[DataCollectionOf(\TestMultiLazyNestedDataWithObjectAndCollection::class)] public Lazy|DataCollection $collection diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index 2f508e12..cba40788 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -7,7 +7,6 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -use Spatie\LaravelData\Tests\Fakes\MultiData; use Spatie\LaravelData\Tests\Fakes\NestedData; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -23,7 +22,7 @@ function findVisibleFields( } it('will hide hidden fields', function () { - $dataClass = new class() extends Data { + $dataClass = new class () extends Data { public string $visible = 'visible'; #[Hidden] @@ -36,7 +35,7 @@ function findVisibleFields( }); it('will hide optional fields which are unitialized', function () { - $dataClass = new class() extends Data { + $dataClass = new class () extends Data { public string $visible = 'visible'; public Optional|string $optional; @@ -50,25 +49,25 @@ function findVisibleFields( // TODO write tests it('can perform an excepts', function () { -// $dataClass = new class() extends Data { -// public function __construct( -// public string $visible = 'visible', -// public SimpleData $simple = new SimpleData('simple'), -// public NestedData $nestedData = new NestedData(new SimpleData('simple')), -// public array $collection = -// ) { -// } -// }; -// -// expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ -// 'multi' => new TransformationContext( -// transformValues: true, -// mapPropertyNames: true, -// wrapExecutionType: true, -// new SplObjectStorage(), -// new SplObjectStorage(), -// new SplObjectStorage(), -// new SplObjectStorage(), -// ), -// ]); + // $dataClass = new class() extends Data { + // public function __construct( + // public string $visible = 'visible', + // public SimpleData $simple = new SimpleData('simple'), + // public NestedData $nestedData = new NestedData(new SimpleData('simple')), + // public array $collection = + // ) { + // } + // }; + // + // expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + // 'multi' => new TransformationContext( + // transformValues: true, + // mapPropertyNames: true, + // wrapExecutionType: true, + // new SplObjectStorage(), + // new SplObjectStorage(), + // new SplObjectStorage(), + // new SplObjectStorage(), + // ), + // ]); }); diff --git a/tests/Support/Partials/ResolvedPartialTest.php b/tests/Support/Partials/ResolvedPartialTest.php index 3f5acf95..6cfd8874 100644 --- a/tests/Support/Partials/ResolvedPartialTest.php +++ b/tests/Support/Partials/ResolvedPartialTest.php @@ -75,7 +75,7 @@ expect($partial->getFields())->toBe(null); }); -it('can use the pointer system when ending in an all', function (){ +it('can use the pointer system when ending in an all', function () { $partial = new ResolvedPartial([ new NestedPartialSegment('struct'), new AllPartialSegment(), From 49a4c6ec5eca92f392fd8bf7f630de9303c5c678 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 8 Jan 2024 09:23:10 +0100 Subject: [PATCH 047/124] Move to custom collections --- benchmarks/DataBench.php | 9 +++ benchmarks/SimpleDataBench.php | 10 +-- src/Concerns/ContextableData.php | 26 ++++++-- src/Concerns/TransformableData.php | 10 ++- .../RequestQueryStringPartialsResolver.php | 6 +- .../Partials/ForwardsToPartialsDefinition.php | 49 +++++++++------ src/Support/Partials/PartialsCollection.php | 13 ++++ .../Partials/ResolvedPartialsCollection.php | 13 ++++ src/Support/Transformation/DataContext.php | 59 ++++++++++-------- .../Transformation/TransformationContext.php | 51 ++++++---------- .../TransformationContextFactory.php | 61 +++++++------------ 11 files changed, 173 insertions(+), 134 deletions(-) create mode 100644 src/Support/Partials/PartialsCollection.php create mode 100644 src/Support/Partials/ResolvedPartialsCollection.php diff --git a/benchmarks/DataBench.php b/benchmarks/DataBench.php index c36e699c..c4ce3338 100644 --- a/benchmarks/DataBench.php +++ b/benchmarks/DataBench.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\LaravelDataServiceProvider; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; use Spatie\LaravelData\Tests\Fakes\MultiNestedData; use Spatie\LaravelData\Tests\Fakes\NestedData; @@ -31,6 +32,14 @@ protected function getPackageProviders($app) ]; } + public function setup() + { + app(DataConfig::class)->getDataClass(ComplicatedData::class); + app(DataConfig::class)->getDataClass(SimpleData::class); + app(DataConfig::class)->getDataClass(MultiNestedData::class); + app(DataConfig::class)->getDataClass(NestedData::class); + } + #[Revs(500), Iterations(2)] public function benchDataCreation() { diff --git a/benchmarks/SimpleDataBench.php b/benchmarks/SimpleDataBench.php index ecf671e4..a0d7bde3 100644 --- a/benchmarks/SimpleDataBench.php +++ b/benchmarks/SimpleDataBench.php @@ -69,9 +69,9 @@ public function benchDataTransformation() $this->data->toArray(); } -// #[Revs(500), Iterations(5), BeforeMethods('setup')] -// public function benchDataManualTransformation() -// { -// $this->data->toUserDefinedToArray(); -// } + #[Revs(5000), Iterations(5), BeforeMethods('setup')] + public function benchDataManualTransformation() + { + $this->data->toUserDefinedToArray(); + } } diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php index 607d18f1..9310e52a 100644 --- a/src/Concerns/ContextableData.php +++ b/src/Concerns/ContextableData.php @@ -5,10 +5,10 @@ use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; use Spatie\LaravelData\Support\Transformation\DataContext; use Spatie\LaravelData\Support\Wrapping\Wrap; use Spatie\LaravelData\Support\Wrapping\WrapType; -use SplObjectStorage; trait ContextableData { @@ -22,24 +22,40 @@ public function getDataContext(): DataContext default => new Wrap(WrapType::UseGlobal), }; - $includedPartials = new SplObjectStorage(); - $excludedPartials = new SplObjectStorage(); - $onlyPartials = new SplObjectStorage(); - $exceptPartials = new SplObjectStorage(); + $includedPartials = null; + $excludedPartials = null; + $onlyPartials = null; + $exceptPartials = null; if ($this instanceof IncludeableDataContract) { + if (! empty($this->includeProperties())) { + $includedPartials = new PartialsCollection(); + } + foreach ($this->includeProperties() as $key => $value) { $includedPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); } + if (! empty($this->excludeProperties())) { + $excludedPartials = new PartialsCollection(); + } + foreach ($this->excludeProperties() as $key => $value) { $excludedPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); } + if (! empty($this->onlyProperties())) { + $onlyPartials = new PartialsCollection(); + } + foreach ($this->onlyProperties() as $key => $value) { $onlyPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); } + if (! empty($this->exceptProperties())) { + $exceptPartials = new PartialsCollection(); + } + foreach ($this->exceptProperties() as $key => $value) { $exceptPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); } diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index a123f23c..186b408b 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -29,27 +29,25 @@ public function transform( $dataContext = $this->getDataContext(); - /** @var TransformationContext $transformationContext */ - - if ($dataContext->includePartials->count() > 0) { + if ($dataContext->includePartials && $dataContext->includePartials->count() > 0) { $transformationContext->mergeIncludedResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->includePartials) ); } - if ($dataContext->excludePartials->count() > 0) { + if ($dataContext->excludePartials && $dataContext->excludePartials->count() > 0) { $transformationContext->mergeExcludedResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->excludePartials) ); } - if ($dataContext->onlyPartials->count() > 0) { + if ($dataContext->onlyPartials && $dataContext->onlyPartials->count() > 0) { $transformationContext->mergeOnlyResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->onlyPartials) ); } - if ($dataContext->exceptPartials->count() > 0) { + if ($dataContext->exceptPartials && $dataContext->exceptPartials->count() > 0) { $transformationContext->mergeExceptResolvedPartials( $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->exceptPartials) ); diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php index 5196eab0..4c5b06e3 100644 --- a/src/Resolvers/RequestQueryStringPartialsResolver.php +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -8,12 +8,12 @@ use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; use Spatie\LaravelData\Support\Partials\PartialType; use Spatie\LaravelData\Support\Partials\Segments\AllPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\FieldsPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\NestedPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\PartialSegment; -use SplObjectStorage; use TypeError; class RequestQueryStringPartialsResolver @@ -27,7 +27,7 @@ public function execute( BaseData|BaseDataCollectable $data, Request $request, PartialType $type - ): ?SplObjectStorage { + ): ?PartialsCollection { $parameter = $type->getRequestParameterName(); if (! $request->has($parameter)) { @@ -40,7 +40,7 @@ public function execute( default => throw new TypeError('Invalid type of data') }); - $partials = new SplObjectStorage(); + $partials = new PartialsCollection(); $partialStrings = is_array($request->get($parameter)) ? $request->get($parameter) diff --git a/src/Support/Partials/ForwardsToPartialsDefinition.php b/src/Support/Partials/ForwardsToPartialsDefinition.php index 96d20972..54b401fb 100644 --- a/src/Support/Partials/ForwardsToPartialsDefinition.php +++ b/src/Support/Partials/ForwardsToPartialsDefinition.php @@ -3,24 +3,25 @@ namespace Spatie\LaravelData\Support\Partials; use Closure; -use SplObjectStorage; trait ForwardsToPartialsDefinition { /** * @return object{ - * includePartials: SplObjectStorage, - * excludePartials: SplObjectStorage, - * onlyPartials: SplObjectStorage, - * exceptPartials: SplObjectStorage, + * includePartials: ?PartialsCollection, + * excludePartials: ?PartialsCollection, + * onlyPartials: ?PartialsCollection, + * exceptPartials: ?PartialsCollection, * } */ abstract protected function getPartialsContainer(): object; public function include(string ...$includes): static { + $partialsCollection = $this->getPartialsContainer()->includePartials ??= new PartialsCollection(); + foreach ($includes as $include) { - $this->getPartialsContainer()->includePartials->attach(Partial::create($include)); + $partialsCollection->attach(Partial::create($include)); } return $this; @@ -28,8 +29,10 @@ public function include(string ...$includes): static public function exclude(string ...$excludes): static { + $partialsCollection = $this->getPartialsContainer()->excludePartials ??= new PartialsCollection(); + foreach ($excludes as $exclude) { - $this->getPartialsContainer()->excludePartials->attach(Partial::create($exclude)); + $partialsCollection->attach(Partial::create($exclude)); } return $this; @@ -37,8 +40,10 @@ public function exclude(string ...$excludes): static public function only(string ...$only): static { + $partialsCollection = $this->getPartialsContainer()->onlyPartials ??= new PartialsCollection(); + foreach ($only as $onlyDefinition) { - $this->getPartialsContainer()->onlyPartials->attach(Partial::create($onlyDefinition)); + $partialsCollection->attach(Partial::create($onlyDefinition)); } return $this; @@ -46,8 +51,10 @@ public function only(string ...$only): static public function except(string ...$except): static { + $partialsCollection = $this->getPartialsContainer()->exceptPartials ??= new PartialsCollection(); + foreach ($except as $exceptDefinition) { - $this->getPartialsContainer()->exceptPartials->attach(Partial::create($exceptDefinition)); + $partialsCollection->attach(Partial::create($exceptDefinition)); } return $this; @@ -55,10 +62,12 @@ public function except(string ...$except): static public function includeWhen(string $include, bool|Closure $condition): static { + $partialsCollection = $this->getPartialsContainer()->includePartials ??= new PartialsCollection(); + if (is_callable($condition)) { - $this->getPartialsContainer()->includePartials->attach(Partial::createConditional($include, $condition)); + $partialsCollection->attach(Partial::createConditional($include, $condition)); } elseif ($condition === true) { - $this->getPartialsContainer()->includePartials->attach(Partial::create($include)); + $partialsCollection->attach(Partial::create($include)); } return $this; @@ -66,10 +75,12 @@ public function includeWhen(string $include, bool|Closure $condition): static public function excludeWhen(string $exclude, bool|Closure $condition): static { + $partialsCollection = $this->getPartialsContainer()->excludePartials ??= new PartialsCollection(); + if (is_callable($condition)) { - $this->getPartialsContainer()->excludePartials->attach(Partial::createConditional($exclude, $condition)); + $partialsCollection->attach(Partial::createConditional($exclude, $condition)); } elseif ($condition === true) { - $this->getPartialsContainer()->excludePartials->attach(Partial::create($exclude)); + $partialsCollection->attach(Partial::create($exclude)); } return $this; @@ -77,10 +88,12 @@ public function excludeWhen(string $exclude, bool|Closure $condition): static public function onlyWhen(string $only, bool|Closure $condition): static { + $partialsCollection = $this->getPartialsContainer()->onlyPartials ??= new PartialsCollection(); + if (is_callable($condition)) { - $this->getPartialsContainer()->onlyPartials->attach(Partial::createConditional($only, $condition)); + $partialsCollection->attach(Partial::createConditional($only, $condition)); } elseif ($condition === true) { - $this->getPartialsContainer()->onlyPartials->attach(Partial::create($only)); + $partialsCollection->attach(Partial::create($only)); } return $this; @@ -88,10 +101,12 @@ public function onlyWhen(string $only, bool|Closure $condition): static public function exceptWhen(string $except, bool|Closure $condition): static { + $partialsCollection = $this->getPartialsContainer()->exceptPartials ??= new PartialsCollection(); + if (is_callable($condition)) { - $this->getPartialsContainer()->exceptPartials->attach(Partial::createConditional($except, $condition)); + $partialsCollection->attach(Partial::createConditional($except, $condition)); } elseif ($condition === true) { - $this->getPartialsContainer()->exceptPartials->attach(Partial::create($except)); + $partialsCollection->attach(Partial::create($except)); } return $this; diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php new file mode 100644 index 00000000..b98a9c7c --- /dev/null +++ b/src/Support/Partials/PartialsCollection.php @@ -0,0 +1,13 @@ + + */ +class PartialsCollection extends SplObjectStorage +{ + +} diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php new file mode 100644 index 00000000..b53eda27 --- /dev/null +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -0,0 +1,13 @@ + + */ +class ResolvedPartialsCollection extends SplObjectStorage +{ + +} diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index e5720cb9..dcdab050 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -4,49 +4,56 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; -use Spatie\LaravelData\Support\Partials\Partial; -use Spatie\LaravelData\Support\Partials\ResolvedPartial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; +use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\Wrap; -use SplObjectStorage; class DataContext { - /** - * @param SplObjectStorage $includePartials - * @param SplObjectStorage $excludePartials - * @param SplObjectStorage $onlyPartials - * @param SplObjectStorage $exceptPartials - */ public function __construct( - public SplObjectStorage $includePartials, - public SplObjectStorage $excludePartials, - public SplObjectStorage $onlyPartials, - public SplObjectStorage $exceptPartials, + public ?PartialsCollection $includePartials, + public ?PartialsCollection $excludePartials, + public ?PartialsCollection $onlyPartials, + public ?PartialsCollection $exceptPartials, public ?Wrap $wrap = null, ) { } public function mergePartials(DataContext $dataContext): self { - $this->includePartials->addAll($dataContext->includePartials); - $this->excludePartials->addAll($dataContext->excludePartials); - $this->onlyPartials->addAll($dataContext->onlyPartials); - $this->exceptPartials->addAll($dataContext->exceptPartials); + if ($dataContext->includePartials) { + $this->includePartials ??= new PartialsCollection(); + + $this->includePartials->addAll($dataContext->includePartials); + } + + if ($dataContext->excludePartials) { + $this->excludePartials ??= new PartialsCollection(); + + $this->excludePartials->addAll($dataContext->excludePartials); + } + + if ($dataContext->onlyPartials) { + $this->onlyPartials ??= new PartialsCollection(); + + $this->onlyPartials->addAll($dataContext->onlyPartials); + } + + if ($dataContext->exceptPartials) { + $this->exceptPartials ??= new PartialsCollection(); + + $this->exceptPartials->addAll($dataContext->exceptPartials); + } return $this; } - /** - * @param SplObjectStorage $partials - * - * @return SplObjectStorage - */ public function getResolvedPartialsAndRemoveTemporaryOnes( BaseData|BaseDataCollectable $data, - SplObjectStorage $partials, - ): SplObjectStorage { - $resolvedPartials = new SplObjectStorage(); - $partialsToDetach = new SplObjectStorage(); + PartialsCollection $partials, + ): ResolvedPartialsCollection { + $resolvedPartials = new ResolvedPartialsCollection(); + $partialsToDetach = new PartialsCollection(); foreach ($partials as $partial) { if ($resolved = $partial->resolve($data)) { diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 9312b816..4248bf05 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -3,26 +3,23 @@ namespace Spatie\LaravelData\Support\Transformation; use Spatie\LaravelData\Support\Partials\ResolvedPartial; +use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use SplObjectStorage; use Stringable; class TransformationContext implements Stringable { /** - * @param null|SplObjectStorage $includedPartials for internal use only - * @param null|SplObjectStorage $excludedPartials for internal use only - * @param null|SplObjectStorage $onlyPartials for internal use only - * @param null|SplObjectStorage $exceptPartials for internal use only + * @note Do not add extra partials here */ public function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public ?SplObjectStorage $includedPartials = null, - public ?SplObjectStorage $excludedPartials = null, - public ?SplObjectStorage $onlyPartials = null, - public ?SplObjectStorage $exceptPartials = null, + public ?ResolvedPartialsCollection $includedPartials = null, + public ?ResolvedPartialsCollection $excludedPartials = null, + public ?ResolvedPartialsCollection $onlyPartials = null, + public ?ResolvedPartialsCollection $exceptPartials = null, ) { } @@ -44,7 +41,7 @@ public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self public function addIncludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void { if ($this->includedPartials === null) { - $this->includedPartials = new SplObjectStorage(); + $this->includedPartials = new ResolvedPartialsCollection(); } foreach ($resolvedPartials as $resolvedPartial) { @@ -55,7 +52,7 @@ public function addIncludedResolvedPartial(ResolvedPartial ...$resolvedPartials) public function addExcludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void { if ($this->excludedPartials === null) { - $this->excludedPartials = new SplObjectStorage(); + $this->excludedPartials = new ResolvedPartialsCollection(); } foreach ($resolvedPartials as $resolvedPartial) { @@ -66,7 +63,7 @@ public function addExcludedResolvedPartial(ResolvedPartial ...$resolvedPartials) public function addOnlyResolvedPartial(ResolvedPartial ...$resolvedPartials): void { if ($this->onlyPartials === null) { - $this->onlyPartials = new SplObjectStorage(); + $this->onlyPartials = new ResolvedPartialsCollection(); } foreach ($resolvedPartials as $resolvedPartial) { @@ -77,7 +74,7 @@ public function addOnlyResolvedPartial(ResolvedPartial ...$resolvedPartials): vo public function addExceptResolvedPartial(ResolvedPartial ...$resolvedPartials): void { if ($this->exceptPartials === null) { - $this->exceptPartials = new SplObjectStorage(); + $this->exceptPartials = new ResolvedPartialsCollection(); } foreach ($resolvedPartials as $resolvedPartial) { @@ -85,49 +82,37 @@ public function addExceptResolvedPartial(ResolvedPartial ...$resolvedPartials): } } - /** - * @param SplObjectStorage $partials - */ - public function mergeIncludedResolvedPartials(SplObjectStorage $partials): void + public function mergeIncludedResolvedPartials(ResolvedPartialsCollection $partials): void { if ($this->includedPartials === null) { - $this->includedPartials = new SplObjectStorage(); + $this->includedPartials = new ResolvedPartialsCollection(); } $this->includedPartials->addAll($partials); } - /** - * @param SplObjectStorage $partials - */ - public function mergeExcludedResolvedPartials(SplObjectStorage $partials): void + public function mergeExcludedResolvedPartials(ResolvedPartialsCollection $partials): void { if ($this->excludedPartials === null) { - $this->excludedPartials = new SplObjectStorage(); + $this->excludedPartials = new ResolvedPartialsCollection(); } $this->excludedPartials->addAll($partials); } - /** - * @param SplObjectStorage $partials - */ - public function mergeOnlyResolvedPartials(SplObjectStorage $partials): void + public function mergeOnlyResolvedPartials(ResolvedPartialsCollection $partials): void { if ($this->onlyPartials === null) { - $this->onlyPartials = new SplObjectStorage(); + $this->onlyPartials = new ResolvedPartialsCollection(); } $this->onlyPartials->addAll($partials); } - /** - * @param SplObjectStorage $partials - */ - public function mergeExceptResolvedPartials(SplObjectStorage $partials): void + public function mergeExceptResolvedPartials(ResolvedPartialsCollection $partials): void { if ($this->exceptPartials === null) { - $this->exceptPartials = new SplObjectStorage(); + $this->exceptPartials = new ResolvedPartialsCollection(); } $this->exceptPartials->addAll($partials); diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index bf3d16cf..89821215 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -6,8 +6,9 @@ use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; +use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use SplObjectStorage; class TransformationContextFactory { @@ -18,20 +19,14 @@ public static function create(): self return new self(); } - /** - * @param ?SplObjectStorage $includedPartials - * @param ?SplObjectStorage $excludedPartials - * @param ?SplObjectStorage $onlyPartials - * @param ?SplObjectStorage $exceptPartials - */ protected function __construct( public bool $transformValues = true, public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - public ?SplObjectStorage $includedPartials = null, - public ?SplObjectStorage $excludedPartials = null, - public ?SplObjectStorage $onlyPartials = null, - public ?SplObjectStorage $exceptPartials = null, + public ?PartialsCollection $includedPartials = null, + public ?PartialsCollection $excludedPartials = null, + public ?PartialsCollection $onlyPartials = null, + public ?PartialsCollection $exceptPartials = null, ) { } @@ -41,7 +36,7 @@ public function get( $includedPartials = null; if ($this->includedPartials) { - $includedPartials = new SplObjectStorage(); + $includedPartials = new ResolvedPartialsCollection(); foreach ($this->includedPartials as $include) { $resolved = $include->resolve($data); @@ -55,7 +50,7 @@ public function get( $excludedPartials = null; if ($this->excludedPartials) { - $excludedPartials = new SplObjectStorage(); + $excludedPartials = new ResolvedPartialsCollection(); foreach ($this->excludedPartials as $exclude) { $resolved = $exclude->resolve($data); @@ -69,7 +64,7 @@ public function get( $onlyPartials = null; if ($this->onlyPartials) { - $onlyPartials = new SplObjectStorage(); + $onlyPartials = new ResolvedPartialsCollection(); foreach ($this->onlyPartials as $only) { $resolved = $only->resolve($data); @@ -83,7 +78,7 @@ public function get( $exceptPartials = null; if ($this->exceptPartials) { - $exceptPartials = new SplObjectStorage(); + $exceptPartials = new ResolvedPartialsCollection(); foreach ($this->exceptPartials as $except) { $resolved = $except->resolve($data); @@ -129,7 +124,7 @@ public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static public function addIncludePartial(Partial ...$partial): static { if ($this->includedPartials === null) { - $this->includedPartials = new SplObjectStorage(); + $this->includedPartials = new PartialsCollection(); } foreach ($partial as $include) { @@ -142,7 +137,7 @@ public function addIncludePartial(Partial ...$partial): static public function addExcludePartial(Partial ...$partial): static { if ($this->excludedPartials === null) { - $this->excludedPartials = new SplObjectStorage(); + $this->excludedPartials = new PartialsCollection(); } foreach ($partial as $exclude) { @@ -155,7 +150,7 @@ public function addExcludePartial(Partial ...$partial): static public function addOnlyPartial(Partial ...$partial): static { if ($this->onlyPartials === null) { - $this->onlyPartials = new SplObjectStorage(); + $this->onlyPartials = new PartialsCollection(); } foreach ($partial as $only) { @@ -168,7 +163,7 @@ public function addOnlyPartial(Partial ...$partial): static public function addExceptPartial(Partial ...$partial): static { if ($this->exceptPartials === null) { - $this->exceptPartials = new SplObjectStorage(); + $this->exceptPartials = new PartialsCollection(); } foreach ($partial as $except) { @@ -178,13 +173,10 @@ public function addExceptPartial(Partial ...$partial): static return $this; } - /** - * @param SplObjectStorage $partials - */ - public function mergeIncludePartials(SplObjectStorage $partials): static + public function mergeIncludePartials(PartialsCollection $partials): static { if ($this->includedPartials === null) { - $this->includedPartials = new SplObjectStorage(); + $this->includedPartials = new PartialsCollection(); } $this->includedPartials->addAll($partials); @@ -192,13 +184,10 @@ public function mergeIncludePartials(SplObjectStorage $partials): static return $this; } - /** - * @param SplObjectStorage $partials - */ - public function mergeExcludePartials(SplObjectStorage $partials): static + public function mergeExcludePartials(PartialsCollection $partials): static { if ($this->excludedPartials === null) { - $this->excludedPartials = new SplObjectStorage(); + $this->excludedPartials = new PartialsCollection(); } $this->excludedPartials->addAll($partials); @@ -206,13 +195,10 @@ public function mergeExcludePartials(SplObjectStorage $partials): static return $this; } - /** - * @param SplObjectStorage $partials - */ - public function mergeOnlyPartials(SplObjectStorage $partials): static + public function mergeOnlyPartials(PartialsCollection $partials): static { if ($this->onlyPartials === null) { - $this->onlyPartials = new SplObjectStorage(); + $this->onlyPartials = new PartialsCollection(); } $this->onlyPartials->addAll($partials); @@ -220,13 +206,10 @@ public function mergeOnlyPartials(SplObjectStorage $partials): static return $this; } - /** - * @param SplObjectStorage $partials - */ - public function mergeExceptPartials(SplObjectStorage $partials): static + public function mergeExceptPartials(PartialsCollection $partials): static { if ($this->exceptPartials === null) { - $this->exceptPartials = new SplObjectStorage(); + $this->exceptPartials = new PartialsCollection(); } $this->exceptPartials->addAll($partials); From b3c899ff6251dff9ac683d49b340d98dfcf855a3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 8 Jan 2024 09:34:07 +0100 Subject: [PATCH 048/124] Wrapping fixes --- .../TransformedDataCollectionResolver.php | 2 +- .../Partials/ResolvedPartialsCollection.php | 13 ++++++- .../Transformation/TransformationContext.php | 36 +++++-------------- tests/DataTest.php | 2 +- 4 files changed, 22 insertions(+), 31 deletions(-) diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index 3c3de1d6..45bb6d9f 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -36,7 +36,7 @@ public function execute( : new Wrap(WrapType::UseGlobal); $nestedContext = $context->wrapExecutionType->shouldExecute() - ? $context->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) + ? (clone $context)->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) : $context; if ($items instanceof DataCollection) { diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php index b53eda27..a4746454 100644 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -3,11 +3,22 @@ namespace Spatie\LaravelData\Support\Partials; use SplObjectStorage; +use Stringable; /** * @extends SplObjectStorage */ -class ResolvedPartialsCollection extends SplObjectStorage +class ResolvedPartialsCollection extends SplObjectStorage implements Stringable { + public function __toString(): string + { + $output = "- excludedPartials:".PHP_EOL; + + foreach ($this as $excludedPartial) { + $output .= " - {$excludedPartial}".PHP_EOL; + } + + return $output; + } } diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 4248bf05..e71562cc 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -25,17 +25,9 @@ public function __construct( public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self { - // Todo: remove and run directly on object - - return new self( - $this->transformValues, - $this->mapPropertyNames, - $wrapExecutionType, - $this->includedPartials, - $this->excludedPartials, - $this->onlyPartials, - $this->exceptPartials, - ); + $this->wrapExecutionType = $wrapExecutionType; + + return $this; } public function addIncludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void @@ -166,7 +158,7 @@ public function __clone(): void public function __toString(): string { - $output = 'Transformation Context '.spl_object_id($this).PHP_EOL; + $output = 'Transformation Context ('.spl_object_id($this).')'.PHP_EOL; $output .= "- wrapExecutionType: {$this->wrapExecutionType->name}".PHP_EOL; @@ -179,31 +171,19 @@ public function __toString(): string } if ($this->includedPartials !== null && $this->includedPartials->count() > 0) { - $output .= "- includedPartials:".PHP_EOL; - foreach ($this->includedPartials as $includedPartial) { - $output .= " - {$includedPartial}".PHP_EOL; - } + $output .= $this->includedPartials; } if ($this->excludedPartials !== null && $this->excludedPartials->count() > 0) { - $output .= "- excludedPartials:".PHP_EOL; - foreach ($this->excludedPartials as $excludedPartial) { - $output .= " - {$excludedPartial}".PHP_EOL; - } + $output .= $this->excludedPartials; } if ($this->onlyPartials !== null && $this->onlyPartials->count() > 0) { - $output .= "- onlyPartials:".PHP_EOL; - foreach ($this->onlyPartials as $onlyPartial) { - $output .= " - {$onlyPartial}".PHP_EOL; - } + $output .= $this->onlyPartials; } if ($this->exceptPartials !== null && $this->exceptPartials->count() > 0) { - $output .= "- exceptPartials:".PHP_EOL; - foreach ($this->exceptPartials as $exceptPartial) { - $output .= " - {$exceptPartial}".PHP_EOL; - } + $output .= $this->exceptPartials; } return $output; diff --git a/tests/DataTest.php b/tests/DataTest.php index cf9f7fcc..63e57860 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -962,7 +962,7 @@ public static function fromData(Data $data) ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); }); -it('can wrap data objects', function () { +it('can wrap data objects by method call', function () { expect( SimpleData::from('Hello World') ->wrap('wrap') From 3553d8836c029c9d398a19d7645fc8349146c239 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Mon, 8 Jan 2024 08:34:32 +0000 Subject: [PATCH 049/124] Fix styling --- src/Support/Partials/PartialsCollection.php | 1 - src/Support/Partials/ResolvedPartialsCollection.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php index b98a9c7c..715ff021 100644 --- a/src/Support/Partials/PartialsCollection.php +++ b/src/Support/Partials/PartialsCollection.php @@ -9,5 +9,4 @@ */ class PartialsCollection extends SplObjectStorage { - } diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php index a4746454..1698816d 100644 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -10,7 +10,6 @@ */ class ResolvedPartialsCollection extends SplObjectStorage implements Stringable { - public function __toString(): string { $output = "- excludedPartials:".PHP_EOL; From 310d7119ae036f931e4ce04da01325e00fb8f76a Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 8 Jan 2024 10:05:33 +0100 Subject: [PATCH 050/124] Latest performance fixes --- benchmarks/DataBench.php | 13 ++-- benchmarks/SimpleDataCollectionBench.php | 77 +++++++++++++++++++++++ src/Enums/DataTypeKind.php | 10 +-- src/Resolvers/TransformedDataResolver.php | 14 +++-- 4 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 benchmarks/SimpleDataCollectionBench.php diff --git a/benchmarks/DataBench.php b/benchmarks/DataBench.php index c4ce3338..3b15d3f5 100644 --- a/benchmarks/DataBench.php +++ b/benchmarks/DataBench.php @@ -15,6 +15,7 @@ use Spatie\LaravelData\Tests\Fakes\MultiNestedData; use Spatie\LaravelData\Tests\Fakes\NestedData; use Spatie\LaravelData\Tests\Fakes\SimpleData; +use function Amp\Iterator\toArray; class DataBench { @@ -34,10 +35,10 @@ protected function getPackageProviders($app) public function setup() { - app(DataConfig::class)->getDataClass(ComplicatedData::class); - app(DataConfig::class)->getDataClass(SimpleData::class); - app(DataConfig::class)->getDataClass(MultiNestedData::class); - app(DataConfig::class)->getDataClass(NestedData::class); + app(DataConfig::class)->getDataClass(ComplicatedData::class)->prepareForCache(); + app(DataConfig::class)->getDataClass(SimpleData::class)->prepareForCache(); + app(DataConfig::class)->getDataClass(MultiNestedData::class)->prepareForCache(); + app(DataConfig::class)->getDataClass(NestedData::class)->prepareForCache(); } #[Revs(500), Iterations(2)] @@ -138,8 +139,8 @@ public function benchDataCollectionTransformation() ) )->all(); - $collection = ComplicatedData::collect($collection, DataCollection::class); + $dataCollection = (new DataCollection(ComplicatedData::class, $collection)); - $collection->toArray(); + $dataCollection->toArray(); } } diff --git a/benchmarks/SimpleDataCollectionBench.php b/benchmarks/SimpleDataCollectionBench.php new file mode 100644 index 00000000..75d7f963 --- /dev/null +++ b/benchmarks/SimpleDataCollectionBench.php @@ -0,0 +1,77 @@ +createApplication(); + } + + protected function getPackageProviders($app) + { + return [ + LaravelDataServiceProvider::class, + ]; + } + + public function setup() + { + $collection = Collection::times( + 15, + fn () => new ComplicatedData( + 42, + 42, + true, + 3.14, + 'Hello World', + [1, 1, 2, 3, 5, 8], + null, + Optional::create(), + 42, + CarbonImmutable::create(1994, 05, 16), + new DateTime('1994-05-16T12:00:00+01:00'), + null, + null, + [] +// new SimpleData('hello'), +// new DataCollection(NestedData::class, [ +// new NestedData(new SimpleData('I')), +// new NestedData(new SimpleData('am')), +// new NestedData(new SimpleData('groot')), +// ]), +// [ +// new NestedData(new SimpleData('I')), +// new NestedData(new SimpleData('am')), +// new NestedData(new SimpleData('groot')), +// ], + )); + + $this->dataCollection = new DataCollection(ComplicatedData::class, $collection); + + app(DataConfig::class)->getDataClass(ComplicatedData::class); + app(DataConfig::class)->getDataClass(SimpleData::class); + } + + #[Revs(500), Iterations(5), BeforeMethods('setup')] + public function benchDataCollectionTransformation() + { + $this->dataCollection->toArray(); + } +} diff --git a/src/Enums/DataTypeKind.php b/src/Enums/DataTypeKind.php index f42fe82f..553e1ab3 100644 --- a/src/Enums/DataTypeKind.php +++ b/src/Enums/DataTypeKind.php @@ -21,14 +21,6 @@ public function isDataObject(): bool public function isDataCollectable(): bool { - return in_array($this, [ - self::DataCollection, - self::DataPaginatedCollection, - self::DataCursorPaginatedCollection, - self::Array, - self::Enumerable, - self::Paginator, - self::CursorPaginator, - ]); + return $this !== self::Default && $this !== self::DataObject; } } diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 11ab3d09..7b359f82 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\DataProperty; @@ -30,7 +31,9 @@ public function execute( BaseData&TransformableData $data, TransformationContext $context, ): array { - $transformed = $this->transform($data, $context); + $dataClass = $this->dataConfig->getDataClass($data::class); + + $transformed = $this->transform($data, $context, $dataClass); if ($data instanceof WrappableData && $context->wrapExecutionType->shouldExecute()) { $transformed = $data->getWrap()->wrap($transformed); @@ -43,12 +46,13 @@ public function execute( return $transformed; } - private function transform(BaseData&TransformableData $data, TransformationContext $context): array - { + private function transform( + BaseData&TransformableData $data, + TransformationContext $context, + DataClass $dataClass, + ): array { $payload = []; - $dataClass = $this->dataConfig->getDataClass($data::class); - $visibleFields = $this->visibleDataFieldsResolver->execute($data, $dataClass, $context); foreach ($dataClass->properties as $property) { From e1d19de0fb02697f0297345613c570f2030e8172 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 8 Jan 2024 10:12:39 +0100 Subject: [PATCH 051/124] Latest performance fixes --- .../TransformedDataCollectionResolver.php | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectionResolver.php index 45bb6d9f..5a62bae0 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectionResolver.php @@ -35,24 +35,26 @@ public function execute( ? $items->getWrap() : new Wrap(WrapType::UseGlobal); - $nestedContext = $context->wrapExecutionType->shouldExecute() - ? (clone $context)->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) + $executeWrap = $context->wrapExecutionType->shouldExecute(); + + $nestedContext = $executeWrap + ? $context->setWrapExecutionType(WrapExecutionType::TemporarilyDisabled) : $context; if ($items instanceof DataCollection) { - return $this->transformItems($items->items(), $wrap, $context, $nestedContext); + return $this->transformItems($items->items(), $wrap, $executeWrap, $nestedContext); } if ($items instanceof Enumerable || is_array($items)) { - return $this->transformItems($items, $wrap, $context, $nestedContext); + return $this->transformItems($items, $wrap, $executeWrap, $nestedContext); } if ($items instanceof PaginatedDataCollection || $items instanceof CursorPaginatedDataCollection) { - return $this->transformPaginator($items->items(), $wrap, $context, $nestedContext); + return $this->transformPaginator($items->items(), $wrap, $nestedContext); } if ($items instanceof Paginator || $items instanceof CursorPaginator) { - return $this->transformPaginator($items, $wrap, $context, $nestedContext); + return $this->transformPaginator($items, $wrap, $nestedContext); } throw new Exception("Cannot transform collection"); @@ -61,7 +63,7 @@ public function execute( protected function transformItems( Enumerable|array $items, Wrap $wrap, - TransformationContext $context, + bool $executeWrap, TransformationContext $nestedContext, ): array { $collection = []; @@ -70,7 +72,7 @@ protected function transformItems( $collection[$key] = $this->transformationClosure($nestedContext)($value); } - return $context->wrapExecutionType->shouldExecute() + return $executeWrap ? $wrap->wrap($collection) : $collection; } @@ -78,12 +80,11 @@ protected function transformItems( protected function transformPaginator( Paginator|CursorPaginator $paginator, Wrap $wrap, - TransformationContext $context, TransformationContext $nestedContext, ): array { $paginator = $paginator->through(fn (BaseData $data) => $this->transformationClosure($nestedContext)($data)); - if ($context->transformValues === false) { + if ($nestedContext->transformValues === false) { return $paginator->all(); } @@ -102,14 +103,14 @@ protected function transformPaginator( } protected function transformationClosure( - TransformationContext $context, + TransformationContext $nestedContext, ): Closure { - return function (BaseData $data) use ($context) { - if (! $data instanceof TransformableData || ! $context->transformValues) { + return function (BaseData $data) use ($nestedContext) { + if (! $data instanceof TransformableData || ! $nestedContext->transformValues) { return $data; } - return $data->transform(clone $context); + return $data->transform(clone $nestedContext); }; } } From c84dd510800341d22868d9af44071c3374ccb6d0 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 8 Jan 2024 13:33:42 +0100 Subject: [PATCH 052/124] 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 53f8e87e..afac760b 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 7b359f82..145b7c71 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 6a6eba63..9d71dda7 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 dd6c2214..3d59e6c8 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 00000000..598456fc --- /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 e71562cc..8cff2bb7 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 89821215..d1ec4f21 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 0dc4a545..766d218e 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 19ad2745..5781d3b6 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 9d30e0d8..afdc32eb 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 092b0014..6ef510f3 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 63e57860..a923ce76 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 15344b27..96e25035 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 c47942ec..85a99631 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 43208bab..66e6ad71 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 d50eacc5..e6b5162f 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 c3db2599..32784cd0 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); }); From 4d1b4134c3bd27a1d9bc09efa5c648ddb1bb2aa4 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 10:40:26 +0100 Subject: [PATCH 053/124] Data config changes --- src/Commands/DataStructuresCacheCommand.php | 2 +- src/Concerns/BaseData.php | 2 +- .../WithDeprecatedCollectionMethod.php | 11 ++- src/DataPipes/CastPropertiesDataPipe.php | 2 +- src/LaravelDataServiceProvider.php | 2 +- src/Resolvers/DataValidationRulesResolver.php | 2 +- src/Resolvers/TransformedDataResolver.php | 2 +- src/Support/Caching/CachedDataConfig.php | 26 ------ src/Support/Casting/GlobalCastsCollection.php | 46 +++++++++ src/Support/DataConfig.php | 93 ++++++++++--------- .../EloquentCasts/DataEloquentCast.php | 4 +- .../DataStructuresCacheCommandTest.php | 2 +- 12 files changed, 112 insertions(+), 82 deletions(-) create mode 100644 src/Support/Casting/GlobalCastsCollection.php diff --git a/src/Commands/DataStructuresCacheCommand.php b/src/Commands/DataStructuresCacheCommand.php index 4e0df8ae..565875b0 100644 --- a/src/Commands/DataStructuresCacheCommand.php +++ b/src/Commands/DataStructuresCacheCommand.php @@ -24,7 +24,7 @@ public function handle( $dataClasses = DataClassFinder::fromConfig(config('data.structure_caching'))->classes(); - $cachedDataConfig = CachedDataConfig::initialize($dataConfig); + $cachedDataConfig = CachedDataConfig::createFromConfig(config('data')); $dataStructureCache->storeConfig($cachedDataConfig); diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index e79c72d9..63dc7e08 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -101,7 +101,7 @@ public function getMorphClass(): string /** @var class-string<\Spatie\LaravelData\Contracts\BaseData> $class */ $class = static::class; - return app(DataConfig::class)->morphMap->getDataClassAlias($class) ?? $class; + return app(DataConfig::class)->morphMap()->getDataClassAlias($class) ?? $class; } public function __sleep(): array diff --git a/src/Concerns/WithDeprecatedCollectionMethod.php b/src/Concerns/WithDeprecatedCollectionMethod.php index 96ef1576..1f91df09 100644 --- a/src/Concerns/WithDeprecatedCollectionMethod.php +++ b/src/Concerns/WithDeprecatedCollectionMethod.php @@ -11,6 +11,11 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; +/** + * @property class-string $_collectionClass + * @property class-string $_paginatedCollectionClass + * @property class-string $_cursorPaginatedCollectionClass + */ trait WithDeprecatedCollectionMethod { /** @deprecated */ @@ -19,20 +24,20 @@ public static function collection(Enumerable|array|AbstractPaginator|Paginator|A if ($items instanceof Paginator || $items instanceof AbstractPaginator) { return static::collect( $items, - property_exists(static::class, '_paginatedCollectionClass') ? static::$_paginatedCollectionClass : PaginatedDataCollection::class + static::$_paginatedCollectionClass ?? PaginatedDataCollection::class ); } if ($items instanceof AbstractCursorPaginator || $items instanceof CursorPaginator) { return static::collect( $items, - property_exists(static::class, '_cursorPaginatedCollectionClass') ? static::$_cursorPaginatedCollectionClass : CursorPaginatedDataCollection::class + static::$_cursorPaginatedCollectionClass ?? CursorPaginatedDataCollection::class ); } return static::collect( $items, - property_exists(static::class, '_collectionClass') ? static::$_collectionClass : DataCollection::class + static::$_collectionClass ?? DataCollection::class ); } } diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index f8fed129..d0283688 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -52,7 +52,7 @@ protected function cast( return $cast->cast($property, $value, $castContext); } - if ($cast = $this->dataConfig->findGlobalCastForProperty($property)) { + if ($cast = $this->dataConfig->globalCasts()->findCastForValue($property)) { return $cast->cast($property, $value, $castContext); } diff --git a/src/LaravelDataServiceProvider.php b/src/LaravelDataServiceProvider.php index 276c676c..be487d3c 100644 --- a/src/LaravelDataServiceProvider.php +++ b/src/LaravelDataServiceProvider.php @@ -31,7 +31,7 @@ public function packageRegistered(): void $this->app->singleton( DataConfig::class, - fn () => $this->app->make(DataStructureCache::class)->getConfig() ?? new DataConfig(config('data')) + fn () => $this->app->make(DataStructureCache::class)->getConfig() ?? DataConfig::createFromConfig(config('data')) ); /** @psalm-suppress UndefinedInterfaceMethod */ diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index 20915c37..f8d0cdb4 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -270,7 +270,7 @@ protected function inferRulesForDataProperty( $path ); - foreach ($this->dataConfig->getRuleInferrers() as $inferrer) { + foreach ($this->dataConfig->ruleInferrers() as $inferrer) { $inferrer->handle($property, $rules, $context); } diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 145b7c71..fc22be5d 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -204,7 +204,7 @@ protected function resolveTransformerForValue( } if ($transformer === null) { - $transformer = $this->dataConfig->transformers->findTransformerForValue($value); + $transformer = $this->dataConfig->globalTransformers()->findTransformerForValue($value); } $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer diff --git a/src/Support/Caching/CachedDataConfig.php b/src/Support/Caching/CachedDataConfig.php index 9a3b527d..789ad8e4 100644 --- a/src/Support/Caching/CachedDataConfig.php +++ b/src/Support/Caching/CachedDataConfig.php @@ -9,15 +9,6 @@ class CachedDataConfig extends DataConfig { protected ?DataStructureCache $cache = null; - public function __construct() - { - parent::__construct([ - 'rule_inferrers' => [], - 'transformers' => [], - 'casts' => [], - ]); // Ensure the parent object is constructed empty, todo v4: remove this and use a better constructor with factory - } - public function getDataClass(string $class): DataClass { return $this->cache?->getDataClass($class) ?? parent::getDataClass($class); @@ -29,21 +20,4 @@ public function setCache(DataStructureCache $cache): self return $this; } - - public static function initialize( - DataConfig $dataConfig - ): self { - $cachedConfig = new self(); - - $cachedConfig->ruleInferrers = $dataConfig->ruleInferrers; - $cachedConfig->transformers = $dataConfig->transformers; - $cachedConfig->casts = $dataConfig->casts; - - $cachedConfig->dataClasses = []; - $cachedConfig->resolvedDataPipelines = []; - - $dataConfig->morphMap->merge($cachedConfig->morphMap); - - return $cachedConfig; - } } diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Casting/GlobalCastsCollection.php new file mode 100644 index 00000000..a2ea56a9 --- /dev/null +++ b/src/Support/Casting/GlobalCastsCollection.php @@ -0,0 +1,46 @@ + $casts + */ + public function __construct( + protected array $casts = [] + ) { + } + + public function add(string $castable, Cast $cast): self + { + $this->casts[ltrim($castable, ' \\')] = $cast; + + return $this; + } + + public function findCastForValue(DataProperty $property): ?Cast + { + foreach ($property->type->type->getAcceptedTypes() as $acceptedType => $baseTypes) { + foreach ([$acceptedType, ...$baseTypes] as $type) { + if ($cast = $this->casts[$type] ?? null) { + return $cast; + } + } + } + + return null; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->casts); + } +} diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index 3d59e6c8..44a0efb6 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -3,85 +3,90 @@ namespace Spatie\LaravelData\Support; use ReflectionClass; -use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\RuleInferrers\RuleInferrer; +use Spatie\LaravelData\Support\Casting\GlobalCastsCollection; use Spatie\LaravelData\Support\Transformation\GlobalTransformersCollection; -use Spatie\LaravelData\Transformers\Transformer; class DataConfig { - /** @var array */ - protected array $dataClasses = []; - - public GlobalTransformersCollection $transformers; - - /** @var array */ - protected array $casts = []; - - /** @var array */ - protected array $resolvedDataPipelines = []; - - /** @var \Spatie\LaravelData\RuleInferrers\RuleInferrer[] */ - protected array $ruleInferrers; - - public readonly DataClassMorphMap $morphMap; - - public function __construct(array $config) + public static function createFromConfig(array $config): static { - $this->ruleInferrers = array_map( + $dataClasses = []; + + $ruleInferrers = array_map( fn (string $ruleInferrerClass) => app($ruleInferrerClass), $config['rule_inferrers'] ?? [] ); - $this->transformers = new GlobalTransformersCollection(); + $transformers = new GlobalTransformersCollection(); foreach ($config['transformers'] ?? [] as $transformable => $transformer) { - $this->transformers->add($transformable, app($transformer)); + $transformers->add($transformable, app($transformer)); } + $casts = new GlobalCastsCollection(); + foreach ($config['casts'] ?? [] as $castable => $cast) { - $this->casts[ltrim($castable, ' \\')] = app($cast); + $casts->add($castable, app($cast)); } - $this->morphMap = new DataClassMorphMap(); + $morphMap = new DataClassMorphMap(); + + return new static( + $transformers, + $casts, + $ruleInferrers, + $morphMap, + $dataClasses, + ); + } + + /** + * @param array $dataClasses + * @param array $resolvedDataPipelines + * @param RuleInferrer[] $ruleInferrers + */ + public function __construct( + protected readonly GlobalTransformersCollection $transformers = new GlobalTransformersCollection(), + protected readonly GlobalCastsCollection $casts = new GlobalCastsCollection(), + protected readonly array $ruleInferrers = [], + protected readonly DataClassMorphMap $morphMap = new DataClassMorphMap(), + protected array $dataClasses = [], + protected array $resolvedDataPipelines = [], + ) { } public function getDataClass(string $class): DataClass { - if (array_key_exists($class, $this->dataClasses)) { - return $this->dataClasses[$class]; - } - - return $this->dataClasses[$class] = DataClass::create(new ReflectionClass($class)); + return $this->dataClasses[$class] ??= DataClass::create(new ReflectionClass($class)); } public function getResolvedDataPipeline(string $class): ResolvedDataPipeline { - if (array_key_exists($class, $this->resolvedDataPipelines)) { - return $this->resolvedDataPipelines[$class]; - } - - return $this->resolvedDataPipelines[$class] = $class::pipeline()->resolve(); + return $this->resolvedDataPipelines[$class] ??= $class::pipeline()->resolve(); } - public function findGlobalCastForProperty(DataProperty $property): ?Cast + public function globalTransformers(): GlobalTransformersCollection { - foreach ($property->type->type->getAcceptedTypes() as $acceptedType => $baseTypes) { - foreach ([$acceptedType, ...$baseTypes] as $type) { - if ($cast = $this->casts[$type] ?? null) { - return $cast; - } - } - } + return $this->transformers; + } - return null; + public function globalCasts(): GlobalCastsCollection + { + return $this->casts; } - public function getRuleInferrers(): array + public function ruleInferrers(): array { return $this->ruleInferrers; } + public function morphMap(): DataClassMorphMap + { + return $this->morphMap; + } + /** * @param array> $map */ diff --git a/src/Support/EloquentCasts/DataEloquentCast.php b/src/Support/EloquentCasts/DataEloquentCast.php index bc14fb5f..95b82e81 100644 --- a/src/Support/EloquentCasts/DataEloquentCast.php +++ b/src/Support/EloquentCasts/DataEloquentCast.php @@ -35,7 +35,7 @@ public function get($model, string $key, $value, array $attributes): ?BaseData if ($this->isAbstractClassCast()) { /** @var class-string $dataClass */ - $dataClass = $this->dataConfig->morphMap->getMorphedDataClass($payload['type']) ?? $payload['type']; + $dataClass = $this->dataConfig->morphMap()->getMorphedDataClass($payload['type']) ?? $payload['type']; return $dataClass::from($payload['data']); } @@ -65,7 +65,7 @@ public function set($model, string $key, $value, array $attributes): ?string if ($isAbstractClassCast) { return json_encode([ - 'type' => $this->dataConfig->morphMap->getDataClassAlias($value::class) ?? $value::class, + 'type' => $this->dataConfig->morphMap()->getDataClassAlias($value::class) ?? $value::class, 'data' => json_decode($value->toJson(), associative: true, flags: JSON_THROW_ON_ERROR), ]); } diff --git a/tests/Commands/DataStructuresCacheCommandTest.php b/tests/Commands/DataStructuresCacheCommandTest.php index 3e538f0c..fcc3ceeb 100644 --- a/tests/Commands/DataStructuresCacheCommandTest.php +++ b/tests/Commands/DataStructuresCacheCommandTest.php @@ -23,7 +23,7 @@ $config = app(DataConfig::class); expect($config)->toBeInstanceOf(CachedDataConfig::class); - expect($config->getRuleInferrers())->toHaveCount(count(config('data.rule_inferrers'))); + expect($config->ruleInferrers())->toHaveCount(count(config('data.rule_inferrers'))); expect(invade($config)->transformers)->toHaveCount(count(config('data.transformers'))); expect(invade($config)->casts)->toHaveCount(count(config('data.casts'))); }); From 2317333c5ae59c2e381b685dce7b2d2bdd6e52e6 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 9 Jan 2024 09:40:50 +0000 Subject: [PATCH 054/124] Fix styling --- src/Support/Casting/GlobalCastsCollection.php | 3 +-- src/Transformers/ArrayableTransformer.php | 1 - tests/DataTest.php | 5 +++-- tests/Fakes/Transformers/ConfidentialDataTransformer.php | 3 ++- tests/Transformers/DateTimeInterfaceTransformerTest.php | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Casting/GlobalCastsCollection.php index a2ea56a9..dec477f3 100644 --- a/src/Support/Casting/GlobalCastsCollection.php +++ b/src/Support/Casting/GlobalCastsCollection.php @@ -6,10 +6,9 @@ use IteratorAggregate; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Transformers\Transformer; use Traversable; -class GlobalCastsCollection implements IteratorAggregate +class GlobalCastsCollection implements IteratorAggregate { /** * @param array $casts diff --git a/src/Transformers/ArrayableTransformer.php b/src/Transformers/ArrayableTransformer.php index 766d218e..b6931b00 100644 --- a/src/Transformers/ArrayableTransformer.php +++ b/src/Transformers/ArrayableTransformer.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Transformers; -use Illuminate\Contracts\Support\Arrayable; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContext; diff --git a/tests/DataTest.php b/tests/DataTest.php index a923ce76..1b5f6669 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -60,6 +60,7 @@ 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 () { @@ -1531,7 +1532,7 @@ public function __construct( ]); it('is possible to add extra global transformers when transforming using context', function () { - $dataClass = new class extends Data { + $dataClass = new class () extends Data { public DateTime $dateTime; }; @@ -1539,7 +1540,7 @@ public function __construct( 'dateTime' => new DateTime(), ]); - $customTransformer = new class implements Transformer { + $customTransformer = new class () implements Transformer { public function transform(DataProperty $property, mixed $value, TransformationContext $context): string { return "Custom transformed date"; diff --git a/tests/Fakes/Transformers/ConfidentialDataTransformer.php b/tests/Fakes/Transformers/ConfidentialDataTransformer.php index 85a99631..677470ac 100644 --- a/tests/Fakes/Transformers/ConfidentialDataTransformer.php +++ b/tests/Fakes/Transformers/ConfidentialDataTransformer.php @@ -2,10 +2,11 @@ namespace Spatie\LaravelData\Tests\Fakes\Transformers; -use Spatie\LaravelData\Support\Transformation\TransformationContext; use function collect; use Spatie\LaravelData\Support\DataProperty; + +use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Transformers\Transformer; class ConfidentialDataTransformer implements Transformer diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index e6b5162f..865bbbb2 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -10,7 +10,7 @@ it('can transform dates', function () { $transformer = new DateTimeInterfaceTransformer(); - $class = new class () extends Data{ + $class = new class () extends Data { public Carbon $carbon; public CarbonImmutable $carbonImmutable; @@ -56,7 +56,7 @@ it('can transform dates with an alternative format', function () { $transformer = new DateTimeInterfaceTransformer(format: 'd-m-Y'); - $class = new class () extends Data{ + $class = new class () extends Data { public Carbon $carbon; public CarbonImmutable $carbonImmutable; From 66b845fc26fb4ea0a254b2266a14616db4013277 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 10:44:13 +0100 Subject: [PATCH 055/124] Data config changes --- src/Concerns/BaseData.php | 2 +- src/DataPipes/CastPropertiesDataPipe.php | 2 +- src/Resolvers/DataValidationRulesResolver.php | 2 +- src/Resolvers/TransformedDataResolver.php | 2 +- src/Support/DataConfig.php | 28 +++---------------- .../EloquentCasts/DataEloquentCast.php | 4 +-- .../DataStructuresCacheCommandTest.php | 2 +- 7 files changed, 11 insertions(+), 31 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 63dc7e08..e79c72d9 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -101,7 +101,7 @@ public function getMorphClass(): string /** @var class-string<\Spatie\LaravelData\Contracts\BaseData> $class */ $class = static::class; - return app(DataConfig::class)->morphMap()->getDataClassAlias($class) ?? $class; + return app(DataConfig::class)->morphMap->getDataClassAlias($class) ?? $class; } public function __sleep(): array diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index d0283688..2494c746 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -52,7 +52,7 @@ protected function cast( return $cast->cast($property, $value, $castContext); } - if ($cast = $this->dataConfig->globalCasts()->findCastForValue($property)) { + if ($cast = $this->dataConfig->casts->findCastForValue($property)) { return $cast->cast($property, $value, $castContext); } diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index f8d0cdb4..da378f79 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -270,7 +270,7 @@ protected function inferRulesForDataProperty( $path ); - foreach ($this->dataConfig->ruleInferrers() as $inferrer) { + foreach ($this->dataConfig->ruleInferrers as $inferrer) { $inferrer->handle($property, $rules, $context); } diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index fc22be5d..145b7c71 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -204,7 +204,7 @@ protected function resolveTransformerForValue( } if ($transformer === null) { - $transformer = $this->dataConfig->globalTransformers()->findTransformerForValue($value); + $transformer = $this->dataConfig->transformers->findTransformerForValue($value); } $shouldUseDefaultDataTransformer = $transformer instanceof ArrayableTransformer diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index 44a0efb6..bf3499b0 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -48,10 +48,10 @@ public static function createFromConfig(array $config): static * @param RuleInferrer[] $ruleInferrers */ public function __construct( - protected readonly GlobalTransformersCollection $transformers = new GlobalTransformersCollection(), - protected readonly GlobalCastsCollection $casts = new GlobalCastsCollection(), - protected readonly array $ruleInferrers = [], - protected readonly DataClassMorphMap $morphMap = new DataClassMorphMap(), + public readonly GlobalTransformersCollection $transformers = new GlobalTransformersCollection(), + public readonly GlobalCastsCollection $casts = new GlobalCastsCollection(), + public readonly array $ruleInferrers = [], + public readonly DataClassMorphMap $morphMap = new DataClassMorphMap(), protected array $dataClasses = [], protected array $resolvedDataPipelines = [], ) { @@ -67,26 +67,6 @@ public function getResolvedDataPipeline(string $class): ResolvedDataPipeline return $this->resolvedDataPipelines[$class] ??= $class::pipeline()->resolve(); } - public function globalTransformers(): GlobalTransformersCollection - { - return $this->transformers; - } - - public function globalCasts(): GlobalCastsCollection - { - return $this->casts; - } - - public function ruleInferrers(): array - { - return $this->ruleInferrers; - } - - public function morphMap(): DataClassMorphMap - { - return $this->morphMap; - } - /** * @param array> $map */ diff --git a/src/Support/EloquentCasts/DataEloquentCast.php b/src/Support/EloquentCasts/DataEloquentCast.php index 95b82e81..bc14fb5f 100644 --- a/src/Support/EloquentCasts/DataEloquentCast.php +++ b/src/Support/EloquentCasts/DataEloquentCast.php @@ -35,7 +35,7 @@ public function get($model, string $key, $value, array $attributes): ?BaseData if ($this->isAbstractClassCast()) { /** @var class-string $dataClass */ - $dataClass = $this->dataConfig->morphMap()->getMorphedDataClass($payload['type']) ?? $payload['type']; + $dataClass = $this->dataConfig->morphMap->getMorphedDataClass($payload['type']) ?? $payload['type']; return $dataClass::from($payload['data']); } @@ -65,7 +65,7 @@ public function set($model, string $key, $value, array $attributes): ?string if ($isAbstractClassCast) { return json_encode([ - 'type' => $this->dataConfig->morphMap()->getDataClassAlias($value::class) ?? $value::class, + 'type' => $this->dataConfig->morphMap->getDataClassAlias($value::class) ?? $value::class, 'data' => json_decode($value->toJson(), associative: true, flags: JSON_THROW_ON_ERROR), ]); } diff --git a/tests/Commands/DataStructuresCacheCommandTest.php b/tests/Commands/DataStructuresCacheCommandTest.php index fcc3ceeb..55f87818 100644 --- a/tests/Commands/DataStructuresCacheCommandTest.php +++ b/tests/Commands/DataStructuresCacheCommandTest.php @@ -23,7 +23,7 @@ $config = app(DataConfig::class); expect($config)->toBeInstanceOf(CachedDataConfig::class); - expect($config->ruleInferrers())->toHaveCount(count(config('data.rule_inferrers'))); + expect($config->ruleInferrers)->toHaveCount(count(config('data.rule_inferrers'))); expect(invade($config)->transformers)->toHaveCount(count(config('data.transformers'))); expect(invade($config)->casts)->toHaveCount(count(config('data.casts'))); }); From 425d7c01d2d6b7c0dc5efe0aa952f63cd5651b7e Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 14:35:24 +0100 Subject: [PATCH 056/124] Small fixes --- phpstan-baseline.neon | 10 ---- src/Concerns/BaseData.php | 10 +++- src/Contracts/BaseData.php | 19 +++++-- src/Resolvers/DataFromSomethingResolver.php | 8 +++ .../RequestQueryStringPartialsResolver.php | 1 - src/Support/DataClass.php | 5 +- src/Support/DataConfig.php | 3 + src/Support/DataContainer.php | 2 +- src/Support/Partials/PartialsCollection.php | 2 +- src/Support/Partials/ResolvedPartial.php | 2 +- .../Partials/ResolvedPartialsCollection.php | 2 +- .../DataTypeScriptTransformer.php | 2 +- tests/Fakes/SimpleDto.php | 18 ++++++ tests/Fakes/SimpleResource.php | 18 ++++++ types/Collection.php | 56 ++++++++++++++----- types/Data.php | 17 ++++++ 16 files changed, 135 insertions(+), 40 deletions(-) create mode 100644 tests/Fakes/SimpleDto.php create mode 100644 tests/Fakes/SimpleResource.php create mode 100644 types/Data.php diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7bea2afd..99a12315 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -95,16 +95,6 @@ parameters: count: 1 path: src/Resolvers/DataFromArrayResolver.php - - - message: "#^Call to an undefined method ReflectionType\\:\\:getName\\(\\)\\.$#" - count: 1 - path: src/Support/DataType.php - - - - message: "#^Parameter \\#1 \\$reflection of method Spatie\\\\LaravelData\\\\Support\\\\DataType\\:\\:resolveDataCollectableType\\(\\) expects ReflectionNamedType, ReflectionType given\\.$#" - count: 1 - path: src/Support/DataType.php - - message: "#^Call to an undefined method DateTimeInterface\\:\\:setTimezone\\(\\)\\.$#" count: 1 diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index e79c72d9..086bcbc3 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -2,12 +2,16 @@ namespace Spatie\LaravelData\Concerns; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Contracts\Pagination\Paginator; +use Illuminate\Contracts\Pagination\CursorPaginator as CursorPaginatorContract; +use Illuminate\Contracts\Pagination\Paginator as PaginatorContract; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Pagination\CursorPaginator; +use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; +use Illuminate\Support\LazyCollection; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; @@ -56,7 +60,7 @@ public static function withoutMagicalCreationFrom(mixed ...$payloads): static ); } - public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator + public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection { return app(DataCollectableFromSomethingResolver::class)->execute( static::class, diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 0aff149f..ed942b63 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -2,11 +2,16 @@ namespace Spatie\LaravelData\Contracts; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Contracts\Pagination\Paginator; +use Illuminate\Contracts\Pagination\CursorPaginator as CursorPaginatorContract; +use Illuminate\Contracts\Pagination\Paginator as PaginatorContract; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; +use Illuminate\Pagination\CursorPaginator; +use Illuminate\Pagination\Paginator; +use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; +use Illuminate\Support\LazyCollection; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; @@ -14,6 +19,7 @@ /** * @template TValue + * @template TKey of array-key */ interface BaseData { @@ -23,16 +29,19 @@ public static function from(mixed ...$payloads): static; public static function withoutMagicalCreationFrom(mixed ...$payloads): static; - public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator; + /** + * @param Collection|EloquentCollection|LazyCollection|Enumerable|array|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|DataCollection $items + * + * @return ($into is 'array' ? array : ($into is class-string ? Collection : ($into is class-string ? Collection : ($into is class-string ? LazyCollection : ($into is class-string ? DataCollection : ($into is class-string ? PaginatedDataCollection : ($into is class-string ? CursorPaginatedDataCollection : ($items is EloquentCollection ? Collection : ($items is Collection ? Collection : ($items is LazyCollection ? LazyCollection : ($items is Enumerable ? Enumerable : ($items is array ? array : ($items is AbstractPaginator ? AbstractPaginator : ($items is PaginatorContract ? PaginatorContract : ($items is AbstractCursorPaginator ? AbstractCursorPaginator : ($items is CursorPaginatorContract ? CursorPaginatorContract : ($items is DataCollection ? DataCollection : ($items is CursorPaginator ? CursorPaginatedDataCollection : ($items is Paginator ? PaginatedDataCollection : DataCollection))))))))))))))))))) */ + public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection; - public static function withoutMagicalCreationCollect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator; + public static function withoutMagicalCreationCollect(mixed $items, ?string $into = null); public static function normalizers(): array; public static function prepareForPipeline(\Illuminate\Support\Collection $properties): \Illuminate\Support\Collection; public static function pipeline(): DataPipeline; - public static function empty(array $extra = []): array; public function getMorphClass(): string; } diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 321ac5ff..bd435fdf 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -9,6 +9,9 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; +/** + * @template TData of BaseData + */ class DataFromSomethingResolver { public function __construct( @@ -33,6 +36,11 @@ public function ignoreMagicalMethods(string ...$methods): self return $this; } + /** + * @param class-string $class + * + * @return TData + */ public function execute(string $class, mixed ...$payloads): BaseData { if ($data = $this->createFromCustomCreationMethod($class, $payloads)) { diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php index 4c5b06e3..4828cd64 100644 --- a/src/Resolvers/RequestQueryStringPartialsResolver.php +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -37,7 +37,6 @@ public function execute( $dataClass = $this->dataConfig->getDataClass(match (true) { $data instanceof BaseData => $data::class, $data instanceof BaseDataCollectable => $data->getDataClass(), - default => throw new TypeError('Invalid type of data') }); $partials = new PartialsCollection(); diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index dc4f09d3..8edc6174 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Contracts\ValidateableData; use Spatie\LaravelData\Contracts\WrappableData; +use Spatie\LaravelData\Data; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Mappers\ProvidedNameMapper; use Spatie\LaravelData\Resolvers\NameMappersResolver; @@ -58,7 +59,7 @@ public function __construct( public static function create(ReflectionClass $class): self { - /** @var class-string $name */ + /** @var class-string $name */ $name = $class->name; $attributes = static::resolveAttributes($class); @@ -192,7 +193,7 @@ protected static function resolveDefaultValues( /** * @param Collection $properties * - * @return LazyDataStructureProperty> + * @return LazyDataStructureProperty> */ protected static function resolveTransformationFields( Collection $properties, diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index bf3499b0..f58e9de7 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -62,6 +62,9 @@ public function getDataClass(string $class): DataClass return $this->dataClasses[$class] ??= DataClass::create(new ReflectionClass($class)); } + /** + * @param class-string $class + */ public function getResolvedDataPipeline(string $class): ResolvedDataPipeline { return $this->resolvedDataPipelines[$class] ??= $class::pipeline()->resolve(); diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index dff50923..af22fb1d 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -23,7 +23,7 @@ private function __construct() public static function get(): DataContainer { if (! isset(static::$instance)) { - static::$instance = new static(); + static::$instance = new self(); } return static::$instance; diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php index 715ff021..e11300c9 100644 --- a/src/Support/Partials/PartialsCollection.php +++ b/src/Support/Partials/PartialsCollection.php @@ -5,7 +5,7 @@ use SplObjectStorage; /** - * @extends SplObjectStorage + * @extends SplObjectStorage */ class PartialsCollection extends SplObjectStorage { diff --git a/src/Support/Partials/ResolvedPartial.php b/src/Support/Partials/ResolvedPartial.php index 05c7386f..bd0d0f32 100644 --- a/src/Support/Partials/ResolvedPartial.php +++ b/src/Support/Partials/ResolvedPartial.php @@ -75,7 +75,7 @@ public function getFields(): ?array /** @return string[] */ public function toLaravel(): array { - /** @var array $partials */ + /** @var array $segments */ $segments = []; for ($i = $this->pointer; $i < $this->segmentCount; $i++) { diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php index 1698816d..1e8807e5 100644 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -6,7 +6,7 @@ use Stringable; /** - * @extends SplObjectStorage + * @extends SplObjectStorage */ class ResolvedPartialsCollection extends SplObjectStorage implements Stringable { diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index eb2e8cd2..504d14c1 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -111,7 +111,7 @@ protected function resolveTypeForProperty( DataTypeKind::DataCollection, DataTypeKind::Array, DataTypeKind::Enumerable => $this->defaultCollectionType($dataProperty->type->dataClass), DataTypeKind::Paginator, DataTypeKind::DataPaginatedCollection => $this->paginatedCollectionType($dataProperty->type->dataClass), DataTypeKind::CursorPaginator, DataTypeKind::DataCursorPaginatedCollection => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), - null => throw new RuntimeException('Cannot end up here since the type is dataCollectable') + default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') }; if ($dataProperty->type->isNullable()) { diff --git a/tests/Fakes/SimpleDto.php b/tests/Fakes/SimpleDto.php new file mode 100644 index 00000000..c6efec03 --- /dev/null +++ b/tests/Fakes/SimpleDto.php @@ -0,0 +1,18 @@ +', $collection); -$collection = SimpleData::collection(collect(['A', 'B'])); -assertType('Spatie\LaravelData\DataCollection<(int|string), Spatie\LaravelData\Tests\Fakes\SimpleData>', $collection); +$collection = SimpleData::collect(['A', 'B']); +assertType('array<'.SimpleData::class.'>', $collection); + +$collection = SimpleData::collect(collect(['A', 'B'])); +assertType(Collection::class.'<(int|string), '.SimpleData::class.'>', $collection); + +$collection = SimpleData::collect(new EloquentCollection([new FakeModel(), new FakeModel()])); +assertType(Collection::class.'<(int|string), '.SimpleData::class.'>', $collection); + +$collection = SimpleData::collect(new LazyCollection(['A', 'B'])); +assertType(LazyCollection::class.'<(int|string), '.SimpleData::class.'>', $collection); + +// Paginators + +$collection = SimpleData::collect(FakeModel::query()->paginate()); +assertType(AbstractPaginator::class.'|'.Enumerable::class.'<(int|string), '.SimpleData::class.'>', $collection); + +$collection = SimpleData::collect(FakeModel::query()->cursorPaginate()); +assertType(PaginatorContract::class.'|'.AbstractCursorPaginator::class.'|'.Enumerable::class.'<(int|string), '.SimpleData::class.'>', $collection); -// PaginatedDataCollection -$paginator = \Illuminate\Database\Eloquent\Model::query()->paginate(); +# into -$collection = SimpleData::collection($paginator); +$collection = SimpleData::collect(collect(['A', 'B']), 'array'); +assertType('array<'.SimpleData::class.'>', $collection); -assertType('Spatie\LaravelData\PaginatedDataCollection<(int|string), Spatie\LaravelData\Tests\Fakes\SimpleData>', $collection); +$collection = SimpleData::collect(['A', 'B'], Collection::class); +assertType(Collection::class.'<(int|string), '.SimpleData::class.'>', $collection); -// CursorPaginatedDataCollection -$paginator = \Illuminate\Database\Eloquent\Model::query()->cursorPaginate(); +$collection = SimpleData::collect(['A', 'B'], DataCollection::class); +assertType(DataCollection::class.'<(int|string), '.SimpleData::class.'>', $collection); -$collection = SimpleData::collection($paginator); +$collection = SimpleData::collect(FakeModel::query()->paginate(), PaginatedDataCollection::class); +assertType(PaginatedDataCollection::class.'<(int|string), '.SimpleData::class.'>', $collection); -assertType('Spatie\LaravelData\CursorPaginatedDataCollection<(int|string), Spatie\LaravelData\Tests\Fakes\SimpleData>', $collection); +$collection = SimpleData::collect(FakeModel::query()->paginate(), CursorPaginatedDataCollection::class); +assertType(CursorPaginatedDataCollection::class.'<(int|string), '.SimpleData::class.'>', $collection); diff --git a/types/Data.php b/types/Data.php new file mode 100644 index 00000000..85285b88 --- /dev/null +++ b/types/Data.php @@ -0,0 +1,17 @@ + Date: Tue, 9 Jan 2024 13:36:16 +0000 Subject: [PATCH 057/124] Fix styling --- src/Concerns/BaseData.php | 1 - src/Resolvers/RequestQueryStringPartialsResolver.php | 1 - src/Support/DataClass.php | 1 - 3 files changed, 3 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 086bcbc3..4663f889 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -4,7 +4,6 @@ use Illuminate\Contracts\Pagination\CursorPaginator as CursorPaginatorContract; use Illuminate\Contracts\Pagination\Paginator as PaginatorContract; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; use Illuminate\Pagination\CursorPaginator; diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php index 4828cd64..7ae11958 100644 --- a/src/Resolvers/RequestQueryStringPartialsResolver.php +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -14,7 +14,6 @@ use Spatie\LaravelData\Support\Partials\Segments\FieldsPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\NestedPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\PartialSegment; -use TypeError; class RequestQueryStringPartialsResolver { diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 8edc6174..286c79a1 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -9,7 +9,6 @@ use ReflectionParameter; use ReflectionProperty; use Spatie\LaravelData\Contracts\AppendableData; -use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\IncludeableData; From 830ec87d14b6a968f62431837a5fee6f49f982c2 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 15:03:45 +0100 Subject: [PATCH 058/124] Fix tests --- composer.json | 9 +++--- tests/Attributes/Validation/PasswordTest.php | 8 ++--- .../DataPipes/CastPropertiesDataPipeTest.php | 4 +-- tests/DataTest.php | 31 ++++++++++++++++++- .../{Attributes => }/RulesDataset.php | 0 5 files changed, 40 insertions(+), 12 deletions(-) rename tests/Datasets/{Attributes => }/RulesDataset.php (100%) diff --git a/composer.json b/composer.json index f46bf673..3a32dbe9 100644 --- a/composer.json +++ b/composer.json @@ -31,15 +31,14 @@ "nette/php-generator": "^3.5", "nunomaduro/larastan": "^2.0", "orchestra/testbench": "^7.6|^8.0", - "pestphp/pest": "^1.22", - "pestphp/pest-plugin-laravel": "^1.3", + "pestphp/pest": "^1.22|^2.0", + "pestphp/pest-plugin-laravel": "^1.3|^2.0", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", - "phpunit/phpunit": "^9.3", + "phpunit/phpunit": "^9.3|^10.0", "spatie/invade": "^1.0", "spatie/laravel-typescript-transformer": "^2.3", - "spatie/pest-plugin-snapshots": "^1.1", - "spatie/phpunit-snapshot-assertions": "^4.2", + "spatie/pest-plugin-snapshots": "^1.1|^2.0", "spatie/test-time": "^1.2" }, "autoload" : { diff --git a/tests/Attributes/Validation/PasswordTest.php b/tests/Attributes/Validation/PasswordTest.php index 18a476b0..528ed3f8 100644 --- a/tests/Attributes/Validation/PasswordTest.php +++ b/tests/Attributes/Validation/PasswordTest.php @@ -19,21 +19,21 @@ function (callable|null $setDefaults, array $expectedConfig) { } )->with(function () { yield 'min length set to 42' => [ - 'setDefaults' => fn () => fn () => ValidationPassword::defaults(fn () => ValidationPassword::min(42)), + 'setDefaults' => fn () => ValidationPassword::defaults(fn () => ValidationPassword::min(42)), 'expectedConfig' => [ 'min' => 42, ], ]; - +// yield 'unconfigured' => [ - 'setDefaults' => fn () => fn () => ValidationPath::create(), + 'setDefaults' => fn () => ValidationPath::create(), 'expectedConfig' => [ 'min' => 8, ], ]; yield 'uncompromised' => [ - 'setDefaults' => fn () => fn () => ValidationPassword::defaults(fn () => ValidationPassword::min(69)->uncompromised(7)), + 'setDefaults' => fn () => ValidationPassword::defaults(fn () => ValidationPassword::min(69)->uncompromised(7)), 'expectedConfig' => [ 'min' => 69, 'uncompromised' => true, diff --git a/tests/DataPipes/CastPropertiesDataPipeTest.php b/tests/DataPipes/CastPropertiesDataPipeTest.php index 4d636ba5..193ddc56 100644 --- a/tests/DataPipes/CastPropertiesDataPipeTest.php +++ b/tests/DataPipes/CastPropertiesDataPipeTest.php @@ -60,7 +60,7 @@ ->float->toEqual(3.14) ->string->toEqual('Hello world') ->array->toEqual([1, 1, 2, 3, 5, 8]) - ->nullable->toBeNull([1, 1, 2, 3, 5, 8]) + ->nullable->toBeNull() ->undefinable->toBeInstanceOf(Optional::class) ->mixed->toEqual(42) ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00')) @@ -110,7 +110,7 @@ ->float->toEqual(3.14) ->string->toEqual('Hello world') ->array->toEqual([1, 1, 2, 3, 5, 8]) - ->nullable->toBeNull([1, 1, 2, 3, 5, 8]) + ->nullable->toBeNull() ->mixed->toBe(42) ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00')) ->explicitCast->toEqual(DateTime::createFromFormat('d-m-Y', '16-06-1994')) diff --git a/tests/DataTest.php b/tests/DataTest.php index 1b5f6669..80ad4af2 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -29,10 +29,12 @@ use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; +use Spatie\LaravelData\Dto; use Spatie\LaravelData\Exceptions\CannotCreateData; use Spatie\LaravelData\Exceptions\CannotSetComputedValue; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Resource; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; @@ -53,6 +55,8 @@ use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedProperty; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithoutConstructor; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithWrap; +use Spatie\LaravelData\Tests\Fakes\SimpleDto; +use Spatie\LaravelData\Tests\Fakes\SimpleResource; use Spatie\LaravelData\Tests\Fakes\Transformers\ConfidentialDataCollectionTransformer; use Spatie\LaravelData\Tests\Fakes\Transformers\ConfidentialDataTransformer; use Spatie\LaravelData\Tests\Fakes\Transformers\StringToUpperTransformer; @@ -60,7 +64,6 @@ 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 () { @@ -1555,3 +1558,29 @@ public function transform(DataProperty $property, mixed $value, TransformationCo 'dateTime' => 'Custom transformed date', ]); }); + +it('can use data as an DTO', function () { + $dto = SimpleDto::from('Hello World'); + + expect($dto)->toBeInstanceOf(SimpleDto::class) + ->toBeInstanceOf(Dto::class) + ->not()->toBeInstanceOf(Data::class) + ->not()->toHaveMethods(['toArray', 'toJson', 'toResponse', 'all', 'include', 'exclude', 'only', 'except', 'transform', 'with', 'jsonSerialize']) + ->and($dto->string)->toEqual('Hello World'); + + expect(fn() => SimpleDto::validate(['string' => null]))->toThrow(ValidationException::class); +}); + +it('can use data as an Resource', function () { + $resource = SimpleResource::from('Hello World'); + + expect($resource)->toBeInstanceOf(SimpleResource::class) + ->toBeInstanceOf(Resource::class) + ->not()->toBeInstanceOf(Data::class) + ->toHaveMethods(['toArray', 'toJson', 'toResponse', 'all', 'include', 'exclude', 'only', 'except', 'transform', 'with', 'jsonSerialize']) + ->and($resource->string)->toEqual('Hello World'); + + expect($resource)->not()->toHaveMethods([ + 'validate' + ]); +}); diff --git a/tests/Datasets/Attributes/RulesDataset.php b/tests/Datasets/RulesDataset.php similarity index 100% rename from tests/Datasets/Attributes/RulesDataset.php rename to tests/Datasets/RulesDataset.php From e5998708bd1e31deb6a215189d6eb94508d7ffbe Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 9 Jan 2024 14:04:14 +0000 Subject: [PATCH 059/124] Fix styling --- tests/Attributes/Validation/PasswordTest.php | 2 +- tests/DataTest.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Attributes/Validation/PasswordTest.php b/tests/Attributes/Validation/PasswordTest.php index 528ed3f8..9e05b685 100644 --- a/tests/Attributes/Validation/PasswordTest.php +++ b/tests/Attributes/Validation/PasswordTest.php @@ -24,7 +24,7 @@ function (callable|null $setDefaults, array $expectedConfig) { 'min' => 42, ], ]; -// + // yield 'unconfigured' => [ 'setDefaults' => fn () => ValidationPath::create(), 'expectedConfig' => [ diff --git a/tests/DataTest.php b/tests/DataTest.php index 80ad4af2..80cc1645 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -64,6 +64,7 @@ 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 () { @@ -1568,7 +1569,7 @@ public function transform(DataProperty $property, mixed $value, TransformationCo ->not()->toHaveMethods(['toArray', 'toJson', 'toResponse', 'all', 'include', 'exclude', 'only', 'except', 'transform', 'with', 'jsonSerialize']) ->and($dto->string)->toEqual('Hello World'); - expect(fn() => SimpleDto::validate(['string' => null]))->toThrow(ValidationException::class); + expect(fn () => SimpleDto::validate(['string' => null]))->toThrow(ValidationException::class); }); it('can use data as an Resource', function () { @@ -1581,6 +1582,6 @@ public function transform(DataProperty $property, mixed $value, TransformationCo ->and($resource->string)->toEqual('Hello World'); expect($resource)->not()->toHaveMethods([ - 'validate' + 'validate', ]); }); From 180a8e8916b26c492407c19ab1a0286e5000ec77 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 15:05:27 +0100 Subject: [PATCH 060/124] Fix tests --- src/Support/DataClass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 286c79a1..05572056 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -199,7 +199,7 @@ protected static function resolveTransformationFields( ): LazyDataStructureProperty { $closure = fn () => $properties ->reject(fn (DataProperty $property): bool => $property->hidden) - ->map(function (DataProperty $property): null|true { + ->map(function (DataProperty $property): null|bool { if ( $property->type->kind->isDataCollectable() || $property->type->kind->isDataObject() From 09756f16ef053535cb5009db78453c0ed6d514b3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 15:07:13 +0100 Subject: [PATCH 061/124] Update composer --- composer.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 3a32dbe9..c2fd6453 100644 --- a/composer.json +++ b/composer.json @@ -31,14 +31,14 @@ "nette/php-generator": "^3.5", "nunomaduro/larastan": "^2.0", "orchestra/testbench": "^7.6|^8.0", - "pestphp/pest": "^1.22|^2.0", - "pestphp/pest-plugin-laravel": "^1.3|^2.0", + "pestphp/pest": "^2.0", + "pestphp/pest-plugin-laravel": "^2.0", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", - "phpunit/phpunit": "^9.3|^10.0", + "phpunit/phpunit": "^10.0", "spatie/invade": "^1.0", "spatie/laravel-typescript-transformer": "^2.3", - "spatie/pest-plugin-snapshots": "^1.1|^2.0", + "spatie/pest-plugin-snapshots": "^2.0", "spatie/test-time": "^1.2" }, "autoload" : { From 7acf70b22af8f276d1bd827ab7ef7689aed31aa8 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 9 Jan 2024 15:09:01 +0100 Subject: [PATCH 062/124] Update composer --- .github/workflows/run-tests.yml | 4 +--- UPGRADING.md | 1 + composer.json | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 5176a086..87590b7c 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,13 +10,11 @@ jobs: matrix: os: [ubuntu-latest] php: [8.1, 8.2, 8.3] - laravel: [9.*, 10.*] + laravel: [10.*] stability: [prefer-lowest, prefer-stable] include: - laravel: 10.* testbench: 8.* - - laravel: 9.* - testbench: 7.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/UPGRADING.md b/UPGRADING.md index afac760b..af2bd95c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -6,6 +6,7 @@ Because there are many breaking changes an upgrade is not that easy. There are m The following things are required when upgrading: +- Laravel 10 is now required - Start by going through your code and replace all static `SomeData::collection($items)` method calls with `SomeData::collect($items, DataCollection::class)` - Use `DataPaginatedCollection::class` when you're expecting a paginated collection - Use `DataCursorPaginatedCollection::class` when you're expecting a cursor paginated collection diff --git a/composer.json b/composer.json index c2fd6453..c5a865f6 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,7 @@ ], "require" : { "php": "^8.1", - "illuminate/contracts": "^9.30|^10.0", + "illuminate/contracts": "^10.0", "phpdocumentor/type-resolver": "^1.5", "spatie/laravel-package-tools": "^1.9.0", "spatie/php-structure-discoverer": "^2.0" @@ -30,7 +30,7 @@ "nesbot/carbon": "^2.63", "nette/php-generator": "^3.5", "nunomaduro/larastan": "^2.0", - "orchestra/testbench": "^7.6|^8.0", + "orchestra/testbench": "^8.0", "pestphp/pest": "^2.0", "pestphp/pest-plugin-laravel": "^2.0", "phpbench/phpbench": "^1.2", From a72ec1476a5baee8e0ea20b1c15e56ffe5705565 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 10 Jan 2024 11:22:32 +0100 Subject: [PATCH 063/124] Move interfaces --- src/Concerns/TransformableData.php | 27 ++----------- src/Contracts/TransformableData.php | 2 +- .../Transformation/TransformationContext.php | 38 +++++++++++++++++++ 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 186b408b..c9dfdb76 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -5,6 +5,7 @@ use Exception; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; +use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\EloquentCasts\DataEloquentCast; use Spatie\LaravelData\Support\Transformation\TransformationContext; @@ -27,30 +28,8 @@ public function transform( default => throw new Exception('Cannot transform data object') }; - $dataContext = $this->getDataContext(); - - if ($dataContext->includePartials && $dataContext->includePartials->count() > 0) { - $transformationContext->mergeIncludedResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->includePartials) - ); - } - - if ($dataContext->excludePartials && $dataContext->excludePartials->count() > 0) { - $transformationContext->mergeExcludedResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->excludePartials) - ); - } - - if ($dataContext->onlyPartials && $dataContext->onlyPartials->count() > 0) { - $transformationContext->mergeOnlyResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->onlyPartials) - ); - } - - if ($dataContext->exceptPartials && $dataContext->exceptPartials->count() > 0) { - $transformationContext->mergeExceptResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($this, $dataContext->exceptPartials) - ); + if ($this instanceof IncludeableDataContract) { + $transformationContext->mergePartialsFromDataContext($this); } return $resolver->execute($this, $transformationContext); diff --git a/src/Contracts/TransformableData.php b/src/Contracts/TransformableData.php index 41cd2ea4..f2b93245 100644 --- a/src/Contracts/TransformableData.php +++ b/src/Contracts/TransformableData.php @@ -9,7 +9,7 @@ use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; -interface TransformableData extends JsonSerializable, Jsonable, Arrayable, EloquentCastable, ContextableData +interface TransformableData extends JsonSerializable, Jsonable, Arrayable, EloquentCastable { public function transform( null|TransformationContextFactory|TransformationContext $transformationContext = null, diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 8cff2bb7..05122ef3 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -2,6 +2,9 @@ namespace Spatie\LaravelData\Support\Transformation; +use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\BaseDataCollectable; +use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; @@ -138,6 +141,41 @@ public function rollBackPartialsWhenRequired(): void } } + /** + * @param IncludeableData&(BaseData|BaseDataCollectable) $data + */ + public function mergePartialsFromDataContext( + IncludeableData $data + ): self { + $dataContext = $data->getDataContext(); + + if ($dataContext->includePartials && $dataContext->includePartials->count() > 0) { + $this->mergeIncludedResolvedPartials( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->includePartials) + ); + } + + if ($dataContext->excludePartials && $dataContext->excludePartials->count() > 0) { + $this->mergeExcludedResolvedPartials( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->excludePartials) + ); + } + + if ($dataContext->onlyPartials && $dataContext->onlyPartials->count() > 0) { + $this->mergeOnlyResolvedPartials( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->onlyPartials) + ); + } + + if ($dataContext->exceptPartials && $dataContext->exceptPartials->count() > 0) { + $this->mergeExceptResolvedPartials( + $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->exceptPartials) + ); + } + + return $this; + } + public function __clone(): void { if ($this->includedPartials !== null) { From d150fe25db6e8c0d65c99e290826da68ede7a214 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 10 Jan 2024 17:29:55 +0100 Subject: [PATCH 064/124] A better way of creating data --- UPGRADING.md | 3 + config/data.php | 7 + phpstan-baseline.neon | 10 - src/Concerns/BaseData.php | 42 ++--- src/Concerns/ValidateableData.php | 6 +- src/Concerns/WireableData.php | 10 +- src/Contracts/BaseData.php | 22 ++- src/Contracts/ValidateableData.php | 2 +- src/DataPipeline.php | 8 - src/DataPipes/AuthorizedDataPipe.php | 9 +- src/DataPipes/CastPropertiesDataPipe.php | 22 ++- src/DataPipes/DataPipe.php | 8 +- src/DataPipes/DefaultValuesDataPipe.php | 9 +- .../FillRouteParameterPropertiesDataPipe.php | 9 +- src/DataPipes/MapPropertiesDataPipe.php | 13 +- src/DataPipes/ValidatePropertiesDataPipe.php | 29 ++- .../DataCollectableFromSomethingResolver.php | 40 ++-- src/Resolvers/DataFromArrayResolver.php | 8 + src/Resolvers/DataFromSomethingResolver.php | 55 +++--- src/Resolvers/EmptyDataResolver.php | 4 +- src/Support/Creation/CreationContext.php | 71 ++++++++ .../Creation/CreationContextFactory.php | 171 ++++++++++++++++++ src/Support/Creation/ValidationType.php | 10 + src/Support/DataClass.php | 3 + src/Support/DataContainer.php | 18 ++ src/Support/DataType.php | 9 + src/Support/ResolvedDataPipeline.php | 5 +- .../Transformation/TransformationContext.php | 2 +- tests/DataTest.php | 1 + .../DataFromSomethingResolverTest.php | 15 +- tests/TestSupport/DataValidationAsserter.php | 3 +- tests/ValidationTest.php | 43 +---- types/Collection.php | 1 + types/Data.php | 1 + types/Factory.php | 30 +++ 35 files changed, 505 insertions(+), 194 deletions(-) create mode 100644 src/Support/Creation/CreationContext.php create mode 100644 src/Support/Creation/CreationContextFactory.php create mode 100644 src/Support/Creation/ValidationType.php create mode 100644 types/Factory.php diff --git a/UPGRADING.md b/UPGRADING.md index af2bd95c..f6cd135c 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -19,6 +19,9 @@ The following things are required when upgrading: - 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 have implemented a custom DataPipe, update the `handle` method signature with the new `TransformationContext` parameter +- If you manually created `ValidatePropertiesDataPipe` using the `allTypes` parameter, please now use the creation context for this +- The `withoutMagicalCreationFrom` method was removed from data in favour for creation by factory - 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/config/data.php b/config/data.php index 6a0c7086..a1ae3c83 100644 --- a/config/data.php +++ b/config/data.php @@ -87,4 +87,11 @@ 'root_namespace' => null, ], ], + + /** + * A data object can be validated when created using a factory or when calling the from + * method. By default, only when a request is passed the data is being validated. This + * behaviour can be changed to always validate or to completely disable validation. + */ + 'validation_type' => \Spatie\LaravelData\Support\Creation\ValidationType::OnlyRequests->value ]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 99a12315..5ef2253d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -65,16 +65,6 @@ parameters: count: 1 path: src/Casts/DateTimeInterfaceCast.php - - - message: "#^Method Spatie\\\\LaravelData\\\\Data\\:\\:from\\(\\) should return static\\(Spatie\\\\LaravelData\\\\Data\\) but returns Spatie\\\\LaravelData\\\\Contracts\\\\BaseData\\.$#" - count: 1 - path: src/Data.php - - - - message: "#^Method Spatie\\\\LaravelData\\\\Data\\:\\:withoutMagicalCreationFrom\\(\\) should return static\\(Spatie\\\\LaravelData\\\\Data\\) but returns Spatie\\\\LaravelData\\\\Contracts\\\\BaseData\\.$#" - count: 1 - path: src/Data.php - - message: "#^PHPDoc tag @return with type Spatie\\\\LaravelData\\\\DataCollection\\ is not subtype of native type static\\(Spatie\\\\LaravelData\\\\DataCollection\\\\)\\.$#" count: 1 diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 4663f889..f46d74eb 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -6,11 +6,10 @@ use Illuminate\Contracts\Pagination\Paginator as PaginatorContract; use Illuminate\Pagination\AbstractCursorPaginator; use Illuminate\Pagination\AbstractPaginator; -use Illuminate\Pagination\CursorPaginator; -use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\LazyCollection; +use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; @@ -23,6 +22,8 @@ use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; +use Spatie\LaravelData\Support\Creation\CreationContext; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; @@ -43,38 +44,25 @@ public static function optional(mixed ...$payloads): ?static return null; } - public static function from(mixed ...$payloads): static + public static function from(mixed ...$payloads): BaseDataContract { - return app(DataFromSomethingResolver::class)->execute( - static::class, - ...$payloads - ); - } - - public static function withoutMagicalCreationFrom(mixed ...$payloads): static - { - return app(DataFromSomethingResolver::class)->withoutMagicalCreation()->execute( - static::class, - ...$payloads - ); + return static::factory()->from(...$payloads); } public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection { - return app(DataCollectableFromSomethingResolver::class)->execute( - static::class, - $items, - $into - ); + return static::factory()->collect($items, $into); } - public static function withoutMagicalCreationCollect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator + public static function factory(?CreationContext $creationContext = null): CreationContextFactory|CreationContext { - return app(DataCollectableFromSomethingResolver::class)->withoutMagicalCreation()->execute( - static::class, - $items, - $into - ); + if ($creationContext) { + $creationContext->dataClass = static::class; + + return $creationContext; + } + + return CreationContextFactory::createFromConfig(static::class); } public static function normalizers(): array @@ -101,7 +89,7 @@ public static function prepareForPipeline(Collection $properties): Collection public function getMorphClass(): string { - /** @var class-string<\Spatie\LaravelData\Contracts\BaseData> $class */ + /** @var class-string $class */ $class = static::class; return app(DataConfig::class)->morphMap->getDataClassAlias($class) ?? $class; diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php index 46b17378..12ff8be3 100644 --- a/src/Concerns/ValidateableData.php +++ b/src/Concerns/ValidateableData.php @@ -49,9 +49,9 @@ public static function validate(Arrayable|array $payload): Arrayable|array public static function validateAndCreate(Arrayable|array $payload): static { - static::validate($payload); - - return static::from($payload); + return static::factory() + ->alwaysValidate() + ->from($payload); } public static function withValidator(Validator $validator): void diff --git a/src/Concerns/WireableData.php b/src/Concerns/WireableData.php index d9348860..6bba68ed 100644 --- a/src/Concerns/WireableData.php +++ b/src/Concerns/WireableData.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Concerns; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; trait WireableData { @@ -13,8 +14,11 @@ public function toLivewire(): array public static function fromLivewire($value): static { - return app(DataFromSomethingResolver::class) - ->ignoreMagicalMethods('fromLivewire') - ->execute(static::class, $value); + /** @var CreationContextFactory $factory */ + $factory = static::factory(); + + return $factory + ->ignoreMagicalMethod('fromLivewire') + ->from($value); } } diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index ed942b63..27a593b5 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -12,10 +12,13 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\LazyCollection; +use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Creation\CreationContext; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; /** * @template TValue @@ -25,21 +28,28 @@ interface BaseData { public static function optional(mixed ...$payloads): ?static; - public static function from(mixed ...$payloads): static; - - public static function withoutMagicalCreationFrom(mixed ...$payloads): static; + /** + * @return static + */ + public static function from(mixed ...$payloads): BaseDataContract; /** * @param Collection|EloquentCollection|LazyCollection|Enumerable|array|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|DataCollection $items * - * @return ($into is 'array' ? array : ($into is class-string ? Collection : ($into is class-string ? Collection : ($into is class-string ? LazyCollection : ($into is class-string ? DataCollection : ($into is class-string ? PaginatedDataCollection : ($into is class-string ? CursorPaginatedDataCollection : ($items is EloquentCollection ? Collection : ($items is Collection ? Collection : ($items is LazyCollection ? LazyCollection : ($items is Enumerable ? Enumerable : ($items is array ? array : ($items is AbstractPaginator ? AbstractPaginator : ($items is PaginatorContract ? PaginatorContract : ($items is AbstractCursorPaginator ? AbstractCursorPaginator : ($items is CursorPaginatorContract ? CursorPaginatorContract : ($items is DataCollection ? DataCollection : ($items is CursorPaginator ? CursorPaginatedDataCollection : ($items is Paginator ? PaginatedDataCollection : DataCollection))))))))))))))))))) */ + * @return ($into is 'array' ? array : ($into is class-string ? Collection : ($into is class-string ? Collection : ($into is class-string ? LazyCollection : ($into is class-string ? DataCollection : ($into is class-string ? PaginatedDataCollection : ($into is class-string ? CursorPaginatedDataCollection : ($items is EloquentCollection ? Collection : ($items is Collection ? Collection : ($items is LazyCollection ? LazyCollection : ($items is Enumerable ? Enumerable : ($items is array ? array : ($items is AbstractPaginator ? AbstractPaginator : ($items is PaginatorContract ? PaginatorContract : ($items is AbstractCursorPaginator ? AbstractCursorPaginator : ($items is CursorPaginatorContract ? CursorPaginatorContract : ($items is DataCollection ? DataCollection : ($items is CursorPaginator ? CursorPaginatedDataCollection : ($items is Paginator ? PaginatedDataCollection : DataCollection))))))))))))))))))) + */ public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection; - public static function withoutMagicalCreationCollect(mixed $items, ?string $into = null); + /** + * @param CreationContext|null $creationContext + * + * @return ($creationContext is null ? CreationContextFactory : CreationContext) + */ + public static function factory(?CreationContext $creationContext = null): CreationContextFactory|CreationContext; public static function normalizers(): array; - public static function prepareForPipeline(\Illuminate\Support\Collection $properties): \Illuminate\Support\Collection; + public static function prepareForPipeline(Collection $properties): Collection; public static function pipeline(): DataPipeline; diff --git a/src/Contracts/ValidateableData.php b/src/Contracts/ValidateableData.php index b46b618a..16551c8c 100644 --- a/src/Contracts/ValidateableData.php +++ b/src/Contracts/ValidateableData.php @@ -9,7 +9,7 @@ interface ValidateableData { public static function validate(Arrayable|array $payload): Arrayable|array; - public static function validateAndCreate(Arrayable|array $payload): object; + public static function validateAndCreate(Arrayable|array $payload): static; public static function withValidator(Validator $validator): void; } diff --git a/src/DataPipeline.php b/src/DataPipeline.php index 114ad439..e008f291 100644 --- a/src/DataPipeline.php +++ b/src/DataPipeline.php @@ -80,12 +80,4 @@ public function resolve(): ResolvedDataPipeline $this->dataConfig->getDataClass($this->classString) ); } - - /** @deprecated */ - public function execute(): Collection - { - return $this->dataConfig - ->getResolvedDataPipeline($this->classString) - ->execute($this->value); - } } diff --git a/src/DataPipes/AuthorizedDataPipe.php b/src/DataPipes/AuthorizedDataPipe.php index 303c4aa2..c044a992 100644 --- a/src/DataPipes/AuthorizedDataPipe.php +++ b/src/DataPipes/AuthorizedDataPipe.php @@ -5,12 +5,17 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; class AuthorizedDataPipe implements DataPipe { - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection - { + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection { if (! $payload instanceof Request) { return $properties; } diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 2494c746..0e160c45 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -5,6 +5,7 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; @@ -16,8 +17,12 @@ public function __construct( ) { } - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection - { + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection { $castContext = $properties->all(); foreach ($properties as $name => $value) { @@ -31,7 +36,7 @@ public function handle(mixed $payload, DataClass $class, Collection $properties) continue; } - $properties[$name] = $this->cast($dataProperty, $value, $castContext); + $properties[$name] = $this->cast($dataProperty, $value, $castContext, $creationContext); } return $properties; @@ -41,6 +46,7 @@ protected function cast( DataProperty $property, mixed $value, array $castContext, + CreationContext $creationContext ): mixed { $shouldCast = $this->shouldBeCasted($property, $value); @@ -52,16 +58,20 @@ protected function cast( return $cast->cast($property, $value, $castContext); } + if ($cast = $creationContext->casts?->findCastForValue($property)) { + return $cast->cast($property, $value, $castContext); + } + if ($cast = $this->dataConfig->casts->findCastForValue($property)) { return $cast->cast($property, $value, $castContext); } - if ($property->type->kind->isDataObject()) { - return $property->type->dataClass::from($value); + if ($property->type->kind->isDataObject() && $property->type->dataClass) { + return $property->type->dataClass::factory($creationContext)->from($value); } if ($property->type->kind->isDataCollectable()) { - return $property->type->dataClass::collect($value, $property->type->dataCollectableClass); + return $property->type->dataClass::factory($creationContext)->collect($value, $property->type->dataCollectableClass); } return $value; diff --git a/src/DataPipes/DataPipe.php b/src/DataPipes/DataPipe.php index b4e109a2..c5ee8df1 100644 --- a/src/DataPipes/DataPipe.php +++ b/src/DataPipes/DataPipe.php @@ -3,9 +3,15 @@ namespace Spatie\LaravelData\DataPipes; use Illuminate\Support\Collection; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; interface DataPipe { - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection; + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection; } diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index 2879cd68..d389f5db 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -4,13 +4,18 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataProperty; class DefaultValuesDataPipe implements DataPipe { - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection - { + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection { $dataDefaults = $class->defaultable ? app()->call([$class->name, 'defaults']) : []; diff --git a/src/DataPipes/FillRouteParameterPropertiesDataPipe.php b/src/DataPipes/FillRouteParameterPropertiesDataPipe.php index 9ac1c422..394836ca 100644 --- a/src/DataPipes/FillRouteParameterPropertiesDataPipe.php +++ b/src/DataPipes/FillRouteParameterPropertiesDataPipe.php @@ -7,13 +7,18 @@ use Spatie\LaravelData\Attributes\FromRouteParameter; use Spatie\LaravelData\Attributes\FromRouteParameterProperty; use Spatie\LaravelData\Exceptions\CannotFillFromRouteParameterPropertyUsingScalarValue; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataProperty; class FillRouteParameterPropertiesDataPipe implements DataPipe { - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection - { + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection { if (! $payload instanceof Request) { return $properties; } diff --git a/src/DataPipes/MapPropertiesDataPipe.php b/src/DataPipes/MapPropertiesDataPipe.php index 8173a6ee..90a5a120 100644 --- a/src/DataPipes/MapPropertiesDataPipe.php +++ b/src/DataPipes/MapPropertiesDataPipe.php @@ -4,12 +4,21 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; class MapPropertiesDataPipe implements DataPipe { - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection - { + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection { + if ($creationContext->mapPropertyNames === false) { + return $properties; + } + foreach ($class->properties as $dataProperty) { if ($dataProperty->inputMappedName === null) { continue; diff --git a/src/DataPipes/ValidatePropertiesDataPipe.php b/src/DataPipes/ValidatePropertiesDataPipe.php index f0baa2ef..cbbc25bd 100644 --- a/src/DataPipes/ValidatePropertiesDataPipe.php +++ b/src/DataPipes/ValidatePropertiesDataPipe.php @@ -4,28 +4,23 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Spatie\LaravelData\Support\Creation\CreationContext; +use Spatie\LaravelData\Support\Creation\ValidationType; use Spatie\LaravelData\Support\DataClass; class ValidatePropertiesDataPipe implements DataPipe { - public function __construct( - protected bool $allTypes = false, - ) { - } - - public static function onlyRequests(): self - { - return new self(false); - } - - public static function allTypes(): self - { - return new self(true); - } + public function handle( + mixed $payload, + DataClass $class, + Collection $properties, + CreationContext $creationContext + ): Collection { + if ($creationContext->validationType === ValidationType::Disabled) { + return $properties; + } - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection - { - if (! $payload instanceof Request && $this->allTypes === false) { + if ($creationContext->validationType === ValidationType::OnlyRequests && ! $payload instanceof Request) { return $properties; } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index d2aefe27..309950e7 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\Exceptions\CannotCreateDataCollectable; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\Creation\CollectableMetaData; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Types\PartialType; @@ -26,27 +27,12 @@ class DataCollectableFromSomethingResolver public function __construct( protected DataConfig $dataConfig, protected DataFromSomethingResolver $dataFromSomethingResolver, - protected bool $withoutMagicalCreation = false, - protected array $ignoredMagicalMethods = [], ) { } - public function withoutMagicalCreation(bool $withoutMagicalCreation = true): self - { - $this->withoutMagicalCreation = $withoutMagicalCreation; - - return $this; - } - - public function ignoreMagicalMethods(string ...$methods): self - { - array_push($this->ignoredMagicalMethods, ...$methods); - - return $this; - } - public function execute( string $dataClass, + CreationContext $creationContext, mixed $items, ?string $into = null, ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { @@ -54,7 +40,7 @@ public function execute( ? PartialType::createFromTypeString($into) : PartialType::createFromValue($items); - $collectable = $this->createFromCustomCreationMethod($dataClass, $items, $into); + $collectable = $this->createFromCustomCreationMethod($dataClass, $creationContext, $items, $into); if ($collectable) { return $collectable; @@ -64,7 +50,7 @@ public function execute( $collectableMetaData = CollectableMetaData::fromOther($items); - $normalizedItems = $this->normalizeItems($items, $dataClass); + $normalizedItems = $this->normalizeItems($items, $dataClass, $creationContext); return match ($intoDataTypeKind) { DataTypeKind::Array => $this->normalizeToArray($normalizedItems), @@ -80,10 +66,11 @@ public function execute( protected function createFromCustomCreationMethod( string $dataClass, + CreationContext $creationContext, mixed $items, ?string $into, ): null|array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { - if ($this->withoutMagicalCreation) { + if ($creationContext->withoutMagicalCreation) { return null; } @@ -106,7 +93,7 @@ protected function createFromCustomCreationMethod( if ($method !== null) { return $dataClass::{$method->name}( - array_map($this->itemsToDataClosure($dataClass), $items) + array_map($this->itemsToDataClosure($dataClass, $creationContext), $items) ); } @@ -116,6 +103,7 @@ protected function createFromCustomCreationMethod( protected function normalizeItems( mixed $items, string $dataClass, + CreationContext $creationContext, ): array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator { if ($items instanceof PaginatedDataCollection || $items instanceof CursorPaginatedDataCollection @@ -128,7 +116,7 @@ protected function normalizeItems( || $items instanceof AbstractPaginator || $items instanceof CursorPaginator || $items instanceof AbstractCursorPaginator) { - return $items->through($this->itemsToDataClosure($dataClass)); + return $items->through($this->itemsToDataClosure($dataClass, $creationContext)); } if ($items instanceof Enumerable) { @@ -137,7 +125,7 @@ protected function normalizeItems( if (is_array($items)) { return array_map( - $this->itemsToDataClosure($dataClass), + $this->itemsToDataClosure($dataClass, $creationContext), $items ); } @@ -187,8 +175,10 @@ protected function normalizeToCursorPaginator( ); } - protected function itemsToDataClosure(string $dataClass): Closure - { - return fn (mixed $data) => $data instanceof $dataClass ? $data : $dataClass::from($data); + protected function itemsToDataClosure( + string $dataClass, + CreationContext $creationContext + ): Closure { + return fn (mixed $data) => $data instanceof $dataClass ? $data : $dataClass::factory($creationContext)->from($data); } } diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index a02ce5f1..ac8f66d8 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -13,12 +13,20 @@ use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataProperty; +/** + * @template TData of BaseData + */ class DataFromArrayResolver { public function __construct(protected DataConfig $dataConfig) { } + /** + * @param class-string $class + * + * @return TData + */ public function execute(string $class, Collection $properties): BaseData { $dataClass = $this->dataConfig->getDataClass($class); diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index bd435fdf..953621e0 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -6,8 +6,8 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\CustomCreationMethodType; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataMethod; /** * @template TData of BaseData @@ -17,33 +17,20 @@ class DataFromSomethingResolver public function __construct( protected DataConfig $dataConfig, protected DataFromArrayResolver $dataFromArrayResolver, - protected bool $withoutMagicalCreation = false, - protected array $ignoredMagicalMethods = [], ) { } - public function withoutMagicalCreation(bool $withoutMagicalCreation = true): self - { - $this->withoutMagicalCreation = $withoutMagicalCreation; - - return $this; - } - - public function ignoreMagicalMethods(string ...$methods): self - { - array_push($this->ignoredMagicalMethods, ...$methods); - - return $this; - } - /** * @param class-string $class * * @return TData */ - public function execute(string $class, mixed ...$payloads): BaseData - { - if ($data = $this->createFromCustomCreationMethod($class, $payloads)) { + public function execute( + string $class, + CreationContext $creationContext, + mixed ...$payloads + ): BaseData { + if ($data = $this->createFromCustomCreationMethod($class, $creationContext, $payloads)) { return $data; } @@ -52,7 +39,7 @@ public function execute(string $class, mixed ...$payloads): BaseData $pipeline = $this->dataConfig->getResolvedDataPipeline($class); foreach ($payloads as $payload) { - foreach ($pipeline->execute($payload) as $key => $value) { + foreach ($pipeline->execute($payload, $creationContext) as $key => $value) { $properties[$key] = $value; } } @@ -60,24 +47,30 @@ public function execute(string $class, mixed ...$payloads): BaseData return $this->dataFromArrayResolver->execute($class, $properties); } - protected function createFromCustomCreationMethod(string $class, array $payloads): ?BaseData - { - if ($this->withoutMagicalCreation) { + protected function createFromCustomCreationMethod( + string $class, + CreationContext $creationContext, + array $payloads + ): ?BaseData { + if ($creationContext->withoutMagicalCreation) { return null; } - /** @var Collection<\Spatie\LaravelData\Support\DataMethod> $customCreationMethods */ $customCreationMethods = $this->dataConfig ->getDataClass($class) - ->methods - ->filter( - fn (DataMethod $method) => $method->customCreationMethodType === CustomCreationMethodType::Object - && ! in_array($method->name, $this->ignoredMagicalMethods) - ); + ->methods; $methodName = null; foreach ($customCreationMethods as $customCreationMethod) { + if ( + $customCreationMethod->customCreationMethodType === CustomCreationMethodType::Object + && $creationContext->ignoredMagicalMethods !== null + && in_array($customCreationMethod->name, $creationContext->ignoredMagicalMethods) + ) { + continue; + } + if ($customCreationMethod->accepts(...$payloads)) { $methodName = $customCreationMethod->name; @@ -93,7 +86,7 @@ protected function createFromCustomCreationMethod(string $class, array $payloads foreach ($payloads as $payload) { if ($payload instanceof Request) { - $pipeline->execute($payload); + $pipeline->execute($payload, $creationContext); } } diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index 3f854ee7..c8b6a93b 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -47,7 +47,9 @@ protected function getValueForProperty(DataProperty $property): mixed return []; } - if ($property->type->kind->isDataObject()) { + if ($property->type->kind->isDataObject() + && $this->dataConfig->getDataClass($property->type->dataClass)->emptyData + ) { return $property->type->dataClass::empty(); } diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php new file mode 100644 index 00000000..397e9c96 --- /dev/null +++ b/src/Support/Creation/CreationContext.php @@ -0,0 +1,71 @@ + $dataClass + */ + public function __construct( + public string $dataClass, + public ValidationType $validationType, + public bool $mapPropertyNames, + public bool $withoutMagicalCreation, + public ?array $ignoredMagicalMethods, + public ?GlobalCastsCollection $casts, + ) { + } + + /** + * @return TData + */ + public function from(mixed ...$payloads): BaseData + { + return DataContainer::get()->dataFromSomethingResolver()->execute( + $this->dataClass, + $this, + ...$payloads + ); + } + + /** + * @template TCollectKey of array-key + * @template TCollectValue + * + * @param Collection|EloquentCollection|LazyCollection|Enumerable|array|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|DataCollection $items + * + * @return ($into is 'array' ? array : ($into is class-string ? Collection : ($into is class-string ? Collection : ($into is class-string ? LazyCollection : ($into is class-string ? DataCollection : ($into is class-string ? PaginatedDataCollection : ($into is class-string ? CursorPaginatedDataCollection : ($items is EloquentCollection ? Collection : ($items is Collection ? Collection : ($items is LazyCollection ? LazyCollection : ($items is Enumerable ? Enumerable : ($items is array ? array : ($items is AbstractPaginator ? AbstractPaginator : ($items is PaginatorContract ? PaginatorContract : ($items is AbstractCursorPaginator ? AbstractCursorPaginator : ($items is CursorPaginatorContract ? CursorPaginatorContract : ($items is DataCollection ? DataCollection : ($items is CursorPaginator ? CursorPaginatedDataCollection : ($items is Paginator ? PaginatedDataCollection : DataCollection))))))))))))))))))) + */ + public function collect( + mixed $items, + ?string $into = null + ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection { + return DataContainer::get()->dataCollectableFromSomethingResolver()->execute( + $this->dataClass, + $this, + $items, + $into + ); + } +} diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php new file mode 100644 index 00000000..efd01752 --- /dev/null +++ b/src/Support/Creation/CreationContextFactory.php @@ -0,0 +1,171 @@ + $dataClass + */ + public function __construct( + public string $dataClass, + public ValidationType $validationType, + public bool $mapPropertyNames, + public bool $withoutMagicalCreation, + public ?array $ignoredMagicalMethods, + public ?GlobalCastsCollection $casts, + ) { + } + + public static function createFromConfig( + string $dataClass, + ?array $config = null + ): self { + $config ??= config('data'); + + return new self( + dataClass: $dataClass, + validationType: ValidationType::from($config['validation_type']), + // TODO: maybe also add these to config, we should do the same for transormation so maybe some config cleanup is needed + mapPropertyNames: true, + withoutMagicalCreation: false, + ignoredMagicalMethods: null, + casts: null, + ); + } + + public function validationType(ValidationType $validationType): self + { + $this->validationType = $validationType; + + return $this; + } + + public function disableValidation(): self + { + $this->validationType = ValidationType::Disabled; + + return $this; + } + + public function onlyValidateRequests(): self + { + $this->validationType = ValidationType::OnlyRequests; + + return $this; + } + + public function alwaysValidate(): self + { + $this->validationType = ValidationType::Always; + + return $this; + } + + public function withoutPropertyNameMapping(bool $withoutPropertyNameMapping = true): self + { + $this->mapPropertyNames = ! $withoutPropertyNameMapping; + + return $this; + } + + public function withoutMagicalCreation(bool $withoutMagicalCreation = true): self + { + $this->withoutMagicalCreation = $withoutMagicalCreation; + + return $this; + } + + public function ignoreMagicalMethod(string ...$methods): self + { + $this->ignoredMagicalMethods ??= []; + + array_push($this->ignoredMagicalMethods, ...$methods); + + return $this; + } + + /** + * @param string $castable + * @param Cast|class-string $cast + */ + public function withCast( + string $castable, + Cast|string $cast, + ): self { + $cast = is_string($cast) ? app($cast) : $cast; + + if ($this->casts === null) { + $this->casts = new GlobalCastsCollection(); + } + + $this->casts->add($castable, $cast); + + return $this; + } + + public function get(): CreationContext + { + return new CreationContext( + dataClass: $this->dataClass, + validationType: $this->validationType, + mapPropertyNames: $this->mapPropertyNames, + withoutMagicalCreation: $this->withoutMagicalCreation, + ignoredMagicalMethods: $this->ignoredMagicalMethods, + casts: $this->casts, + ); + } + + /** + * @return TData + */ + public function from(mixed ...$payloads): BaseData + { + return DataContainer::get()->dataFromSomethingResolver()->execute( + $this->dataClass, + $this->get(), + ...$payloads + ); + } + + /** + * @template TCollectKey of array-key + * @template TCollectValue + * + * @param Collection|EloquentCollection|LazyCollection|Enumerable|array|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|DataCollection $items + * + * @return ($into is 'array' ? array : ($into is class-string ? Collection : ($into is class-string ? Collection : ($into is class-string ? LazyCollection : ($into is class-string ? DataCollection : ($into is class-string ? PaginatedDataCollection : ($into is class-string ? CursorPaginatedDataCollection : ($items is EloquentCollection ? Collection : ($items is Collection ? Collection : ($items is LazyCollection ? LazyCollection : ($items is Enumerable ? Enumerable : ($items is array ? array : ($items is AbstractPaginator ? AbstractPaginator : ($items is PaginatorContract ? PaginatorContract : ($items is AbstractCursorPaginator ? AbstractCursorPaginator : ($items is CursorPaginatorContract ? CursorPaginatorContract : ($items is DataCollection ? DataCollection : ($items is CursorPaginator ? CursorPaginatedDataCollection : ($items is Paginator ? PaginatedDataCollection : DataCollection))))))))))))))))))) + */ + public function collect( + mixed $items, + ?string $into = null + ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection { + return DataContainer::get()->dataCollectableFromSomethingResolver()->execute( + $this->dataClass, + $this->get(), + $items, + $into + ); + } +} diff --git a/src/Support/Creation/ValidationType.php b/src/Support/Creation/ValidationType.php new file mode 100644 index 00000000..fbef2165 --- /dev/null +++ b/src/Support/Creation/ValidationType.php @@ -0,0 +1,10 @@ +implementsInterface(ValidateableData::class), defaultable: $class->implementsInterface(DefaultableData::class), wrappable: $class->implementsInterface(WrappableData::class), + emptyData: $class->implementsInterface(EmptyData::class), attributes: $attributes, dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index af22fb1d..3003a376 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -2,6 +2,8 @@ namespace Spatie\LaravelData\Support; +use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; +use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; @@ -16,6 +18,10 @@ class DataContainer protected ?RequestQueryStringPartialsResolver $requestQueryStringPartialsResolver = null; + protected ?DataFromSomethingResolver $dataFromSomethingResolver = null; + + protected ?DataCollectableFromSomethingResolver $dataCollectableFromSomethingResolver = null; + private function __construct() { } @@ -44,10 +50,22 @@ public function requestQueryStringPartialsResolver(): RequestQueryStringPartials return $this->requestQueryStringPartialsResolver ??= app(RequestQueryStringPartialsResolver::class); } + public function dataFromSomethingResolver(): DataFromSomethingResolver + { + return $this->dataFromSomethingResolver ??= app(DataFromSomethingResolver::class); + } + + public function dataCollectableFromSomethingResolver(): DataCollectableFromSomethingResolver + { + return $this->dataCollectableFromSomethingResolver ??= app(DataCollectableFromSomethingResolver::class); + } + public function reset() { $this->transformedDataResolver = null; $this->transformedDataCollectionResolver = null; $this->requestQueryStringPartialsResolver = null; + $this->dataFromSomethingResolver = null; + $this->dataCollectableFromSomethingResolver = null; } } diff --git a/src/Support/DataType.php b/src/Support/DataType.php index aa42ad73..ab669c05 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -2,11 +2,20 @@ namespace Spatie\LaravelData\Support; +use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\Types\Type; class DataType { + /** + * @param Type $type + * @param string|null $lazyType + * @param bool $isOptional + * @param DataTypeKind $kind + * @param class-string|null $dataClass + * @param string|null $dataCollectableClass + */ public function __construct( public readonly Type $type, public readonly ?string $lazyType, diff --git a/src/Support/ResolvedDataPipeline.php b/src/Support/ResolvedDataPipeline.php index 8c1cb1fe..4ae2cf55 100644 --- a/src/Support/ResolvedDataPipeline.php +++ b/src/Support/ResolvedDataPipeline.php @@ -4,6 +4,7 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Exceptions\CannotCreateData; +use Spatie\LaravelData\Support\Creation\CreationContext; class ResolvedDataPipeline { @@ -18,7 +19,7 @@ public function __construct( ) { } - public function execute(mixed $value): Collection + public function execute(mixed $value, CreationContext $creationContext): Collection { $properties = null; @@ -39,7 +40,7 @@ public function execute(mixed $value): Collection $properties = ($this->dataClass->name)::prepareForPipeline($properties); foreach ($this->pipes as $pipe) { - $piped = $pipe->handle($value, $this->dataClass, $properties); + $piped = $pipe->handle($value, $this->dataClass, $properties, $creationContext); $properties = $piped; } diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 05122ef3..f906f5b6 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -13,7 +13,7 @@ class TransformationContext implements Stringable { /** - * @note Do not add extra partials here + * @note Do not add extra partials here manually */ public function __construct( public bool $transformValues = true, diff --git a/tests/DataTest.php b/tests/DataTest.php index 80cc1645..2c40bea7 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1585,3 +1585,4 @@ public function transform(DataProperty $property, mixed $value, TransformationCo 'validate', ]); }); + diff --git a/tests/Resolvers/DataFromSomethingResolverTest.php b/tests/Resolvers/DataFromSomethingResolverTest.php index f59a0d41..de05a816 100644 --- a/tests/Resolvers/DataFromSomethingResolverTest.php +++ b/tests/Resolvers/DataFromSomethingResolverTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Model; use Spatie\LaravelData\Data; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMultipleArgumentCreationMethod; use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; @@ -102,7 +103,7 @@ public static function fromArray(array $payload) }; expect( - $data::withoutMagicalCreationFrom(['hash_id' => 1, 'name' => 'Taylor']) + $data::factory()->withoutMagicalCreation()->from(['hash_id' => 1, 'name' => 'Taylor']) )->toEqual(new $data(null, 'Taylor')); expect($data::from(['hash_id' => 1, 'name' => 'Taylor'])) @@ -128,10 +129,18 @@ public static function fromArray(array $payload) } expect( - app(DataFromSomethingResolver::class)->ignoreMagicalMethods('fromArray')->execute(DummyA::class, ['hash_id' => 1, 'name' => 'Taylor']) + app(DataFromSomethingResolver::class)->execute( + DummyA::class, + CreationContextFactory::createFromConfig(DummyA::class)->ignoreMagicalMethod('fromArray')->get(), + ['hash_id' => 1, 'name' => 'Taylor'] + ) )->toEqual(new DummyA(null, 'Taylor')); expect( - app(DataFromSomethingResolver::class)->execute(DummyA::class, ['hash_id' => 1, 'name' => 'Taylor']) + app(DataFromSomethingResolver::class)->execute( + DummyA::class, + CreationContextFactory::createFromConfig(DummyA::class)->get(), + ['hash_id' => 1, 'name' => 'Taylor'] + ) )->toEqual(new DummyA(1, 'Taylor')); }); diff --git a/tests/TestSupport/DataValidationAsserter.php b/tests/TestSupport/DataValidationAsserter.php index 15270611..1a6245de 100644 --- a/tests/TestSupport/DataValidationAsserter.php +++ b/tests/TestSupport/DataValidationAsserter.php @@ -5,6 +5,7 @@ use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationRuleParser; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use function PHPUnit\Framework\assertTrue; use Spatie\LaravelData\Data; @@ -163,7 +164,7 @@ private function pipePayload(array $payload): array ->through(MapPropertiesDataPipe::class) ->through(ValidatePropertiesDataPipe::class) ->resolve() - ->execute($payload); + ->execute($payload, CreationContextFactory::createFromConfig($this->dataClass)->get()); return $properties->all(); } diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 20180e22..bedbb65e 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -12,10 +12,6 @@ use Illuminate\Validation\Rules\In as LaravelIn; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; - -use function Pest\Laravel\mock; -use function PHPUnit\Framework\assertFalse; - use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapName; @@ -38,17 +34,7 @@ use Spatie\LaravelData\Attributes\WithoutValidation; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; -use Spatie\LaravelData\DataPipeline; -use Spatie\LaravelData\DataPipes\AuthorizedDataPipe; -use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe; -use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\Mappers\SnakeCaseMapper; -use Spatie\LaravelData\Normalizers\ArrayableNormalizer; -use Spatie\LaravelData\Normalizers\ArrayNormalizer; -use Spatie\LaravelData\Normalizers\ModelNormalizer; -use Spatie\LaravelData\Normalizers\ObjectNormalizer; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Validation\References\FieldReference; @@ -69,6 +55,8 @@ use Spatie\LaravelData\Tests\Fakes\Support\FakeInjectable; use Spatie\LaravelData\Tests\Fakes\ValidationAttributes\PassThroughCustomValidationAttribute; use Spatie\LaravelData\Tests\TestSupport\DataValidationAsserter; +use function Pest\Laravel\mock; +use function PHPUnit\Framework\assertFalse; it('can validate a string', function () { $dataClass = new class () extends Data { @@ -2099,29 +2087,8 @@ public static function fromRequest(Request $request): self it('can validate non-requests payloads', function () { $dataClass = new class () extends Data { - public static bool $validateAllTypes = false; - #[In('Hello World')] public string $string; - - public static function pipeline(): DataPipeline - { - return DataPipeline::create() - ->into(static::class) - ->normalizer(ModelNormalizer::class) - ->normalizer(ArrayableNormalizer::class) - ->normalizer(ObjectNormalizer::class) - ->normalizer(ArrayNormalizer::class) - ->through(AuthorizedDataPipe::class) - ->through( - self::$validateAllTypes - ? ValidatePropertiesDataPipe::allTypes() - : ValidatePropertiesDataPipe::onlyRequests() - ) - ->through(MapPropertiesDataPipe::class) - ->through(DefaultValuesDataPipe::class) - ->through(CastPropertiesDataPipe::class); - } }; $data = $dataClass::from([ @@ -2131,11 +2098,7 @@ public static function pipeline(): DataPipeline expect($data)->toBeInstanceOf(Data::class) ->string->toEqual('nowp'); - app(DataConfig::class)->reset(); - - $dataClass::$validateAllTypes = true; - - $data = $dataClass::from([ + $data = $dataClass::factory()->alwaysValidate()->from([ 'string' => 'nowp', ]); })->throws(ValidationException::class); diff --git a/types/Collection.php b/types/Collection.php index 6aac1dd3..b6eeb770 100644 --- a/types/Collection.php +++ b/types/Collection.php @@ -53,3 +53,4 @@ $collection = SimpleData::collect(FakeModel::query()->paginate(), CursorPaginatedDataCollection::class); assertType(CursorPaginatedDataCollection::class.'<(int|string), '.SimpleData::class.'>', $collection); + diff --git a/types/Data.php b/types/Data.php index 85285b88..877b3314 100644 --- a/types/Data.php +++ b/types/Data.php @@ -2,6 +2,7 @@ /** @noinspection PhpExpressionResultUnusedInspection */ +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDto; use Spatie\LaravelData\Tests\Fakes\SimpleResource; diff --git a/types/Factory.php b/types/Factory.php new file mode 100644 index 00000000..b8c8beea --- /dev/null +++ b/types/Factory.php @@ -0,0 +1,30 @@ +', $factory); + +$factory = SimpleDto::factory(CreationContextFactory::createFromConfig(SimpleData::class)->get()); +assertType(CreationContext::class.'<'.SimpleDto::class.'>' , $factory); // From SimpleData to SimpleDto + +// Data + +$data = SimpleData::factory()->from('Hello World'); +assertType(SimpleData::class, $data); + +// Collection + +$collection = SimpleData::factory()->collect(['A', 'B']); +assertType('array', $collection); + +$collection = SimpleData::factory()->collect(['A', 'B'], into: DataCollection::class); +assertType(DataCollection::class.'', $collection); From 6a1251d0708347ed82bc765cc92c5d8d3d6b2274 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 11 Jan 2024 12:27:35 +0100 Subject: [PATCH 065/124] Baseline updated --- phpstan-baseline.neon | 60 +++++++++++++++++++++++++ src/Concerns/ValidateableData.php | 3 +- src/Contracts/ValidateableData.php | 6 ++- src/Resolvers/DataValidatorResolver.php | 5 ++- src/Resolvers/EmptyDataResolver.php | 21 +++++---- src/Support/DataType.php | 1 + 6 files changed, 84 insertions(+), 12 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5ef2253d..d314109d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -65,11 +65,26 @@ parameters: count: 1 path: src/Casts/DateTimeInterfaceCast.php + - + message: "#^Instanceof between \\*NEVER\\* and Spatie\\\\LaravelData\\\\Contracts\\\\BaseDataCollectable will always evaluate to false\\.$#" + count: 1 + path: src/Data.php + + - + message: "#^Method Spatie\\\\LaravelData\\\\Data\\:\\:validateAndCreate\\(\\) should return static\\(Spatie\\\\LaravelData\\\\Data\\) but returns Spatie\\\\LaravelData\\\\Contracts\\\\BaseData\\.$#" + count: 1 + path: src/Data.php + - message: "#^PHPDoc tag @return with type Spatie\\\\LaravelData\\\\DataCollection\\ is not subtype of native type static\\(Spatie\\\\LaravelData\\\\DataCollection\\\\)\\.$#" count: 1 path: src/DataCollection.php + - + message: "#^Method Spatie\\\\LaravelData\\\\Dto\\:\\:validateAndCreate\\(\\) should return static\\(Spatie\\\\LaravelData\\\\Dto\\) but returns Spatie\\\\LaravelData\\\\Contracts\\\\BaseData\\.$#" + count: 1 + path: src/Dto.php + - message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Pagination\\\\Paginator\\:\\:count\\(\\)\\.$#" count: 1 @@ -85,6 +100,51 @@ parameters: count: 1 path: src/Resolvers/DataFromArrayResolver.php + - + message: "#^PHPDoc tag @var for variable \\$dataClass has invalid type Spatie\\\\LaravelData\\\\Concerns\\\\EmptyData\\.$#" + count: 1 + path: src/Resolvers/EmptyDataResolver.php + + - + message: "#^Instanceof between \\*NEVER\\* and Spatie\\\\LaravelData\\\\Contracts\\\\BaseDataCollectable will always evaluate to false\\.$#" + count: 1 + path: src/Resource.php + + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: src/Support/DataConfig.php + + - + message: "#^Match expression does not handle remaining values\\: \\(class\\-string\\&literal\\-string\\)\\|\\(class\\-string\\&literal\\-string\\)$#" + count: 1 + path: src/Support/Factories/DataTypeFactory.php + + - + message: "#^Match expression does not handle remaining values\\: \\(class\\-string\\&literal\\-string\\)\\|\\(class\\-string\\&literal\\-string\\)$#" + count: 1 + path: src/Support/Factories/DataTypeFactory.php + + - + message: "#^Parameter \\#1 \\$storage of method SplObjectStorage\\\\:\\:removeAll\\(\\) expects SplObjectStorage\\, Spatie\\\\LaravelData\\\\Support\\\\Partials\\\\PartialsCollection given\\.$#" + count: 1 + path: src/Support/Transformation/DataContext.php + + - + message: "#^Call to an undefined method ReflectionType\\:\\:getName\\(\\)\\.$#" + count: 2 + path: src/Support/Types/MultiType.php + + - + message: "#^Parameter \\#1 \\$type of static method Spatie\\\\LaravelData\\\\Support\\\\Types\\\\PartialType\\:\\:create\\(\\) expects ReflectionNamedType, ReflectionType given\\.$#" + count: 1 + path: src/Support/Types/MultiType.php + + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: src/Support/Types/MultiType.php + - message: "#^Call to an undefined method DateTimeInterface\\:\\:setTimezone\\(\\)\\.$#" count: 1 diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php index 12ff8be3..384b8a56 100644 --- a/src/Concerns/ValidateableData.php +++ b/src/Concerns/ValidateableData.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; +use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; use Spatie\LaravelData\Resolvers\DataValidationRulesResolver; use Spatie\LaravelData\Resolvers\DataValidatorResolver; use Spatie\LaravelData\Support\Validation\DataRules; @@ -47,7 +48,7 @@ public static function validate(Arrayable|array $payload): Arrayable|array return $validator->validated(); } - public static function validateAndCreate(Arrayable|array $payload): static + public static function validateAndCreate(Arrayable|array $payload): ValidateableDataContract { return static::factory() ->alwaysValidate() diff --git a/src/Contracts/ValidateableData.php b/src/Contracts/ValidateableData.php index 16551c8c..94585c2e 100644 --- a/src/Contracts/ValidateableData.php +++ b/src/Contracts/ValidateableData.php @@ -4,12 +4,16 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Validation\Validator; +use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; interface ValidateableData { public static function validate(Arrayable|array $payload): Arrayable|array; - public static function validateAndCreate(Arrayable|array $payload): static; + /** + * @return static + */ + public static function validateAndCreate(Arrayable|array $payload): ValidateableDataContract; public static function withValidator(Validator $validator): void; } diff --git a/src/Resolvers/DataValidatorResolver.php b/src/Resolvers/DataValidatorResolver.php index 0a58846b..b0fbf642 100644 --- a/src/Resolvers/DataValidatorResolver.php +++ b/src/Resolvers/DataValidatorResolver.php @@ -5,7 +5,8 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Facades\Validator as ValidatorFacade; use Illuminate\Validation\Validator; -use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\ValidateableData; use Spatie\LaravelData\Support\Validation\DataRules; use Spatie\LaravelData\Support\Validation\ValidationPath; @@ -17,7 +18,7 @@ public function __construct( ) { } - /** @param class-string $dataClass */ + /** @param class-string $dataClass */ public function execute(string $dataClass, Arrayable|array $payload): Validator { $payload = $payload instanceof Arrayable ? $payload->toArray() : $payload; diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index c8b6a93b..15cf8062 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData\Resolvers; +use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Exceptions\DataPropertyCanOnlyHaveOneType; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; @@ -35,29 +36,33 @@ public function execute(string $class, array $extra = []): array protected function getValueForProperty(DataProperty $property): mixed { - if ($property->type->isMixed()) { + $propertyType = $property->type; + if ($propertyType->isMixed()) { return null; } - if ($property->type->type instanceof MultiType && $property->type->type->acceptedTypesCount() > 1) { + if ($propertyType->type instanceof MultiType && $propertyType->type->acceptedTypesCount() > 1) { throw DataPropertyCanOnlyHaveOneType::create($property); } - if ($property->type->type->acceptsType('array')) { + if ($propertyType->type->acceptsType('array')) { return []; } - if ($property->type->kind->isDataObject() - && $this->dataConfig->getDataClass($property->type->dataClass)->emptyData + if ($propertyType->kind->isDataObject() + && $this->dataConfig->getDataClass($propertyType->dataClass)->emptyData ) { - return $property->type->dataClass::empty(); + /** @var class-string $dataClass */ + $dataClass = $propertyType->dataClass; + + return $dataClass::empty(); } - if ($property->type->kind->isDataCollectable()) { + if ($propertyType->kind->isDataCollectable()) { return []; } - if ($property->type->type->findAcceptedTypeForBaseType(Traversable::class) !== null) { + if ($propertyType->type->findAcceptedTypeForBaseType(Traversable::class) !== null) { return []; } diff --git a/src/Support/DataType.php b/src/Support/DataType.php index ab669c05..4e0f7350 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\Types\Type; From 0e9b0a5dd38221dd05043c6685180c2ba6cfec14 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 11 Jan 2024 11:28:03 +0000 Subject: [PATCH 066/124] Fix styling --- src/Concerns/BaseData.php | 2 -- src/Concerns/WireableData.php | 1 - src/DataPipeline.php | 1 - src/Resolvers/EmptyDataResolver.php | 2 +- src/Support/DataType.php | 1 - tests/DataTest.php | 1 - tests/TestSupport/DataValidationAsserter.php | 3 ++- tests/ValidationTest.php | 7 ++++--- 8 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index f46d74eb..58965bb8 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -20,8 +20,6 @@ use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; -use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataConfig; diff --git a/src/Concerns/WireableData.php b/src/Concerns/WireableData.php index 6bba68ed..86749467 100644 --- a/src/Concerns/WireableData.php +++ b/src/Concerns/WireableData.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Concerns; -use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Support\Creation\CreationContextFactory; trait WireableData diff --git a/src/DataPipeline.php b/src/DataPipeline.php index e008f291..912697ba 100644 --- a/src/DataPipeline.php +++ b/src/DataPipeline.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData; -use Illuminate\Support\Collection; use Spatie\LaravelData\DataPipes\DataPipe; use Spatie\LaravelData\Normalizers\Normalizer; use Spatie\LaravelData\Support\DataConfig; diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index 15cf8062..de9fc01a 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -52,7 +52,7 @@ protected function getValueForProperty(DataProperty $property): mixed if ($propertyType->kind->isDataObject() && $this->dataConfig->getDataClass($propertyType->dataClass)->emptyData ) { - /** @var class-string $dataClass */ + /** @var class-string $dataClass */ $dataClass = $propertyType->dataClass; return $dataClass::empty(); diff --git a/src/Support/DataType.php b/src/Support/DataType.php index 4e0f7350..ab669c05 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Support; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\Types\Type; diff --git a/tests/DataTest.php b/tests/DataTest.php index 2c40bea7..80cc1645 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1585,4 +1585,3 @@ public function transform(DataProperty $property, mixed $value, TransformationCo 'validate', ]); }); - diff --git a/tests/TestSupport/DataValidationAsserter.php b/tests/TestSupport/DataValidationAsserter.php index 1a6245de..1e8a5df4 100644 --- a/tests/TestSupport/DataValidationAsserter.php +++ b/tests/TestSupport/DataValidationAsserter.php @@ -5,7 +5,6 @@ use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationRuleParser; -use Spatie\LaravelData\Support\Creation\CreationContextFactory; use function PHPUnit\Framework\assertTrue; use Spatie\LaravelData\Data; @@ -13,10 +12,12 @@ use Spatie\LaravelData\DataPipeline; use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; + use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\Normalizers\ArrayNormalizer; use Spatie\LaravelData\Resolvers\DataValidationRulesResolver; use Spatie\LaravelData\Resolvers\DataValidatorResolver; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\Validation\DataRules; use Spatie\LaravelData\Support\Validation\ValidationPath; diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index bedbb65e..61428765 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -12,6 +12,10 @@ use Illuminate\Validation\Rules\In as LaravelIn; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; + +use function Pest\Laravel\mock; +use function PHPUnit\Framework\assertFalse; + use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapName; @@ -36,7 +40,6 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Optional; -use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Validation\References\FieldReference; use Spatie\LaravelData\Support\Validation\References\RouteParameterReference; use Spatie\LaravelData\Support\Validation\ValidationContext; @@ -55,8 +58,6 @@ use Spatie\LaravelData\Tests\Fakes\Support\FakeInjectable; use Spatie\LaravelData\Tests\Fakes\ValidationAttributes\PassThroughCustomValidationAttribute; use Spatie\LaravelData\Tests\TestSupport\DataValidationAsserter; -use function Pest\Laravel\mock; -use function PHPUnit\Framework\assertFalse; it('can validate a string', function () { $dataClass = new class () extends Data { From 76c7522cb95cbd077774dc9dd9fa5c71cb0347f3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 11 Jan 2024 12:48:26 +0100 Subject: [PATCH 067/124] Creation context in casts --- UPGRADING.md | 3 + src/Casts/Cast.php | 4 +- src/Casts/DateTimeInterfaceCast.php | 4 +- src/Casts/EnumCast.php | 4 +- src/Concerns/BaseData.php | 2 +- src/Concerns/ValidateableData.php | 2 +- src/Contracts/BaseData.php | 2 +- src/Contracts/ValidateableData.php | 2 +- src/DataPipes/CastPropertiesDataPipe.php | 12 ++- src/Support/Casting/GlobalCastsCollection.php | 7 ++ .../Creation/CreationContextFactory.php | 14 ++++ tests/Casts/DateTimeInterfaceCastTest.php | 75 +++++++++++-------- tests/Casts/EnumCastTest.php | 25 ++++++- tests/DataTest.php | 2 +- tests/Fakes/Castables/SimpleCastable.php | 4 +- tests/Fakes/Casts/ConfidentialDataCast.php | 4 +- .../Casts/ConfidentialDataCollectionCast.php | 4 +- tests/Fakes/Casts/ContextAwareCast.php | 6 +- tests/Fakes/Casts/StringToUpperCast.php | 4 +- 19 files changed, 125 insertions(+), 55 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index f6cd135c..3e8b9f79 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -19,6 +19,9 @@ The following things are required when upgrading: - 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 have implemented a custom `Cast` + - The `$castContext` parameter is renamed to `$properties` and changed it type from `array` to `collection` + - A new `$creationContext` parameter is added of type `CreationContext` - If you have implemented a custom DataPipe, update the `handle` method signature with the new `TransformationContext` parameter - If you manually created `ValidatePropertiesDataPipe` using the `allTypes` parameter, please now use the creation context for this - The `withoutMagicalCreationFrom` method was removed from data in favour for creation by factory diff --git a/src/Casts/Cast.php b/src/Casts/Cast.php index 028241be..84c16d34 100644 --- a/src/Casts/Cast.php +++ b/src/Casts/Cast.php @@ -2,9 +2,11 @@ namespace Spatie\LaravelData\Casts; +use Illuminate\Support\Collection; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; interface Cast { - public function cast(DataProperty $property, mixed $value, array $context): mixed; + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $creationContext): mixed; } diff --git a/src/Casts/DateTimeInterfaceCast.php b/src/Casts/DateTimeInterfaceCast.php index 575308e4..6a4bdf49 100644 --- a/src/Casts/DateTimeInterfaceCast.php +++ b/src/Casts/DateTimeInterfaceCast.php @@ -4,7 +4,9 @@ use DateTimeInterface; use DateTimeZone; +use Illuminate\Support\Collection; use Spatie\LaravelData\Exceptions\CannotCastDate; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; class DateTimeInterfaceCast implements Cast @@ -17,7 +19,7 @@ public function __construct( ) { } - public function cast(DataProperty $property, mixed $value, array $context): DateTimeInterface|Uncastable + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): DateTimeInterface|Uncastable { $formats = collect($this->format ?? config('data.date_format')); diff --git a/src/Casts/EnumCast.php b/src/Casts/EnumCast.php index bd0c1b64..996fc804 100644 --- a/src/Casts/EnumCast.php +++ b/src/Casts/EnumCast.php @@ -3,7 +3,9 @@ namespace Spatie\LaravelData\Casts; use BackedEnum; +use Illuminate\Support\Collection; use Spatie\LaravelData\Exceptions\CannotCastEnum; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; use Throwable; @@ -14,7 +16,7 @@ public function __construct( ) { } - public function cast(DataProperty $property, mixed $value, array $context): BackedEnum | Uncastable + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): BackedEnum | Uncastable { $type = $this->type ?? $property->type->type->findAcceptedTypeForBaseType(BackedEnum::class); diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 58965bb8..bc0b85b9 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -42,7 +42,7 @@ public static function optional(mixed ...$payloads): ?static return null; } - public static function from(mixed ...$payloads): BaseDataContract + public static function from(mixed ...$payloads): static { return static::factory()->from(...$payloads); } diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php index 384b8a56..e1852fcf 100644 --- a/src/Concerns/ValidateableData.php +++ b/src/Concerns/ValidateableData.php @@ -48,7 +48,7 @@ public static function validate(Arrayable|array $payload): Arrayable|array return $validator->validated(); } - public static function validateAndCreate(Arrayable|array $payload): ValidateableDataContract + public static function validateAndCreate(Arrayable|array $payload): static { return static::factory() ->alwaysValidate() diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 27a593b5..9dae4827 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -31,7 +31,7 @@ public static function optional(mixed ...$payloads): ?static; /** * @return static */ - public static function from(mixed ...$payloads): BaseDataContract; + public static function from(mixed ...$payloads): static; /** * @param Collection|EloquentCollection|LazyCollection|Enumerable|array|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|DataCollection $items diff --git a/src/Contracts/ValidateableData.php b/src/Contracts/ValidateableData.php index 94585c2e..a926fc0a 100644 --- a/src/Contracts/ValidateableData.php +++ b/src/Contracts/ValidateableData.php @@ -13,7 +13,7 @@ public static function validate(Arrayable|array $payload): Arrayable|array; /** * @return static */ - public static function validateAndCreate(Arrayable|array $payload): ValidateableDataContract; + public static function validateAndCreate(Arrayable|array $payload): static; public static function withValidator(Validator $validator): void; } diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 0e160c45..2cf6158d 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -23,8 +23,6 @@ public function handle( Collection $properties, CreationContext $creationContext ): Collection { - $castContext = $properties->all(); - foreach ($properties as $name => $value) { $dataProperty = $class->properties->first(fn (DataProperty $dataProperty) => $dataProperty->name === $name); @@ -36,7 +34,7 @@ public function handle( continue; } - $properties[$name] = $this->cast($dataProperty, $value, $castContext, $creationContext); + $properties[$name] = $this->cast($dataProperty, $value, $properties, $creationContext); } return $properties; @@ -45,7 +43,7 @@ public function handle( protected function cast( DataProperty $property, mixed $value, - array $castContext, + Collection $properties, CreationContext $creationContext ): mixed { $shouldCast = $this->shouldBeCasted($property, $value); @@ -55,15 +53,15 @@ protected function cast( } if ($cast = $property->cast) { - return $cast->cast($property, $value, $castContext); + return $cast->cast($property, $value, $properties, $creationContext); } if ($cast = $creationContext->casts?->findCastForValue($property)) { - return $cast->cast($property, $value, $castContext); + return $cast->cast($property, $value, $properties, $creationContext); } if ($cast = $this->dataConfig->casts->findCastForValue($property)) { - return $cast->cast($property, $value, $castContext); + return $cast->cast($property, $value, $properties, $creationContext); } if ($property->type->kind->isDataObject() && $property->type->dataClass) { diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Casting/GlobalCastsCollection.php index dec477f3..0d647ec4 100644 --- a/src/Support/Casting/GlobalCastsCollection.php +++ b/src/Support/Casting/GlobalCastsCollection.php @@ -24,6 +24,13 @@ public function add(string $castable, Cast $cast): self return $this; } + + public function merge(self $casts): self + { + $this->casts = array_merge($this->casts, $casts->casts); + + return $this; + } public function findCastForValue(DataProperty $property): ?Cast { diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index efd01752..12be1c65 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -125,6 +125,20 @@ public function withCast( return $this; } + public function withCastCollection( + GlobalCastsCollection $casts, + ): self { + if ($this->casts === null) { + $this->casts = $casts; + + return $this; + } + + $this->casts->merge($casts); + + return $this; + } + public function get(): CreationContext { return new CreationContext( diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 8301a65c..4b3e6a60 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -5,6 +5,7 @@ use Carbon\CarbonTimeZone; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Casts\Uncastable; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; it('can cast date times', function () { @@ -24,7 +25,8 @@ $caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbon')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new Carbon('19-05-1994 00:00:00')); @@ -32,7 +34,8 @@ $caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new CarbonImmutable('19-05-1994 00:00:00')); @@ -40,7 +43,8 @@ $caster->cast( DataProperty::create(new ReflectionProperty($class, 'dateTime')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new DateTime('19-05-1994 00:00:00')); @@ -48,7 +52,8 @@ $caster->cast( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new DateTimeImmutable('19-05-1994 00:00:00')); }); @@ -64,7 +69,8 @@ $caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbon')), '19-05-1994', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new DateTime('19-05-1994 00:00:00')); })->throws(Exception::class); @@ -80,7 +86,8 @@ $caster->cast( DataProperty::create(new ReflectionProperty($class, 'int')), '1994-05-16 12:20:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(Uncastable::create()); }); @@ -101,34 +108,38 @@ expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbon')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') - ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') - ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'dateTime')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') - ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') - ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); }); it('can cast date times with a timezone', function () { @@ -147,32 +158,36 @@ expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbon')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') - ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') - ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'dateTime')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') - ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); expect($caster->cast( DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), '19-05-1994 00:00:00', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() )) - ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') - ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); }); diff --git a/tests/Casts/EnumCastTest.php b/tests/Casts/EnumCastTest.php index d9272fad..a9ded990 100644 --- a/tests/Casts/EnumCastTest.php +++ b/tests/Casts/EnumCastTest.php @@ -2,6 +2,7 @@ use Spatie\LaravelData\Casts\EnumCast; use Spatie\LaravelData\Casts\Uncastable; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\Enums\DummyUnitEnum; @@ -19,7 +20,8 @@ $this->caster->cast( DataProperty::create(new ReflectionProperty($class, 'enum')), 'foo', - [] + collect(), + CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(DummyBackedEnum::FOO); }); @@ -30,7 +32,12 @@ }; expect( - $this->caster->cast(DataProperty::create(new ReflectionProperty($class, 'enum')), 'bar', []) + $this->caster->cast( + DataProperty::create(new ReflectionProperty($class, 'enum')), + 'bar', + collect(), + CreationContextFactory::createFromConfig($class::class)->get() + ) )->toEqual(DummyBackedEnum::FOO); })->throws(Exception::class); @@ -40,7 +47,12 @@ }; expect( - $this->caster->cast(DataProperty::create(new ReflectionProperty($class, 'enum')), 'foo', []) + $this->caster->cast( + DataProperty::create(new ReflectionProperty($class, 'enum')), + 'foo', + collect(), + CreationContextFactory::createFromConfig($class::class)->get() + ) )->toEqual(Uncastable::create()); }); @@ -50,7 +62,12 @@ }; expect( - $this->caster->cast(DataProperty::create(new ReflectionProperty($class, 'int')), 'foo', []) + $this->caster->cast( + DataProperty::create(new ReflectionProperty($class, 'int')), + 'foo', + collect(), + CreationContextFactory::createFromConfig($class::class)->get(), + ) ) ->toEqual(Uncastable::create()); }); diff --git a/tests/DataTest.php b/tests/DataTest.php index 80cc1645..d1794b98 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -917,7 +917,7 @@ public function __construct( ]); expect($data)->casted - ->toEqual('json:+{"nested":"Hello","string":"world","casted":"json:"}'); + ->toEqual('json:+{"nested":{"string":"Hello"},"string":"world","casted":"json:"}'); }); it('will transform native enums', function () { diff --git a/tests/Fakes/Castables/SimpleCastable.php b/tests/Fakes/Castables/SimpleCastable.php index 417d4919..4124c6f2 100644 --- a/tests/Fakes/Castables/SimpleCastable.php +++ b/tests/Fakes/Castables/SimpleCastable.php @@ -2,8 +2,10 @@ namespace Spatie\LaravelData\Tests\Fakes\Castables; +use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Casts\Castable; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; class SimpleCastable implements Castable @@ -15,7 +17,7 @@ public function __construct(public string $value) public static function dataCastUsing(...$arguments): Cast { return new class () implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): mixed + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed { return new SimpleCastable($value); } diff --git a/tests/Fakes/Casts/ConfidentialDataCast.php b/tests/Fakes/Casts/ConfidentialDataCast.php index 55fd1c58..c551ee66 100644 --- a/tests/Fakes/Casts/ConfidentialDataCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCast.php @@ -2,13 +2,15 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; +use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Tests\Fakes\SimpleData; class ConfidentialDataCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): SimpleData + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): SimpleData { return SimpleData::from('CONFIDENTIAL'); } diff --git a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php index bfbbcb23..d991ab2a 100644 --- a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php @@ -2,13 +2,15 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; +use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Tests\Fakes\SimpleData; class ConfidentialDataCollectionCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): array + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): array { return array_map(fn () => SimpleData::from('CONFIDENTIAL'), $value); } diff --git a/tests/Fakes/Casts/ContextAwareCast.php b/tests/Fakes/Casts/ContextAwareCast.php index 6f12b21a..6d1976d3 100644 --- a/tests/Fakes/Casts/ContextAwareCast.php +++ b/tests/Fakes/Casts/ContextAwareCast.php @@ -2,13 +2,15 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; +use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; class ContextAwareCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): mixed + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed { - return $value . '+' . json_encode($context); + return $value . '+' . $properties->toJson(); } } diff --git a/tests/Fakes/Casts/StringToUpperCast.php b/tests/Fakes/Casts/StringToUpperCast.php index a38ab487..3e2fac6a 100644 --- a/tests/Fakes/Casts/StringToUpperCast.php +++ b/tests/Fakes/Casts/StringToUpperCast.php @@ -2,12 +2,14 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; +use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; class StringToUpperCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): string + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): string { return strtoupper($value); } From 82f1983ab4f5f0cb5c82f2916fc99a4fd15dc382 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 11 Jan 2024 11:48:53 +0000 Subject: [PATCH 068/124] Fix styling --- src/Concerns/ValidateableData.php | 1 - src/Contracts/BaseData.php | 1 - src/Contracts/ValidateableData.php | 1 - src/Support/Casting/GlobalCastsCollection.php | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php index e1852fcf..12ff8be3 100644 --- a/src/Concerns/ValidateableData.php +++ b/src/Concerns/ValidateableData.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; -use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; use Spatie\LaravelData\Resolvers\DataValidationRulesResolver; use Spatie\LaravelData\Resolvers\DataValidatorResolver; use Spatie\LaravelData\Support\Validation\DataRules; diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 9dae4827..bd316446 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -12,7 +12,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\LazyCollection; -use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; diff --git a/src/Contracts/ValidateableData.php b/src/Contracts/ValidateableData.php index a926fc0a..8319a1c7 100644 --- a/src/Contracts/ValidateableData.php +++ b/src/Contracts/ValidateableData.php @@ -4,7 +4,6 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Validation\Validator; -use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; interface ValidateableData { diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Casting/GlobalCastsCollection.php index 0d647ec4..65fc0374 100644 --- a/src/Support/Casting/GlobalCastsCollection.php +++ b/src/Support/Casting/GlobalCastsCollection.php @@ -24,7 +24,7 @@ public function add(string $castable, Cast $cast): self return $this; } - + public function merge(self $casts): self { $this->casts = array_merge($this->casts, $casts->casts); From 7cc6e0e58901f6c66fbf6d2f12ecd6363ead948b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 15 Jan 2024 12:57:50 +0100 Subject: [PATCH 069/124] wip --- UPGRADING.md | 2 +- src/Attributes/WithCastAndTransformer.php | 36 +++++++++++++++++++ src/Casts/Cast.php | 2 +- src/Concerns/ResponsableData.php | 5 --- src/Contracts/ResponsableData.php | 12 ++++++- src/CursorPaginatedDataCollection.php | 6 ++-- src/PaginatedDataCollection.php | 6 ++-- src/Support/DataProperty.php | 3 +- .../CastTransformers/FakeCastTransformer.php | 24 +++++++++++++ tests/Support/DataPropertyTest.php | 12 +++++++ 10 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 src/Attributes/WithCastAndTransformer.php create mode 100644 tests/Fakes/CastTransformers/FakeCastTransformer.php diff --git a/UPGRADING.md b/UPGRADING.md index 3e8b9f79..a062bfa5 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -21,7 +21,7 @@ The following things are required when upgrading: - If you have implemented a custom `Transformer`, update the `transform` method signature with the new `TransformationContext` parameter - If you have implemented a custom `Cast` - The `$castContext` parameter is renamed to `$properties` and changed it type from `array` to `collection` - - A new `$creationContext` parameter is added of type `CreationContext` + - A new `$context` parameter is added of type `CreationContext` - If you have implemented a custom DataPipe, update the `handle` method signature with the new `TransformationContext` parameter - If you manually created `ValidatePropertiesDataPipe` using the `allTypes` parameter, please now use the creation context for this - The `withoutMagicalCreationFrom` method was removed from data in favour for creation by factory diff --git a/src/Attributes/WithCastAndTransformer.php b/src/Attributes/WithCastAndTransformer.php new file mode 100644 index 00000000..9976bcb3 --- /dev/null +++ b/src/Attributes/WithCastAndTransformer.php @@ -0,0 +1,36 @@ + $class */ + public string $class, + mixed ...$arguments + ) { + if (! is_a($this->class, Transformer::class, true)) { + throw CannotCreateTransformerAttribute::notATransformer(); + } + + if (! is_a($this->class, Cast::class, true)) { + throw CannotCreateCastAttribute::notACast(); + } + + $this->arguments = $arguments; + } + + public function get(): Cast&Transformer + { + return new ($this->class)(...$this->arguments); + } +} diff --git a/src/Casts/Cast.php b/src/Casts/Cast.php index 84c16d34..fa87837d 100644 --- a/src/Casts/Cast.php +++ b/src/Casts/Cast.php @@ -8,5 +8,5 @@ interface Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $creationContext): mixed; + public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed; } diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index e65ed6df..68dfed0d 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -12,11 +12,6 @@ trait ResponsableData { - /** - * @param \Illuminate\Http\Request $request - * - * @return \Symfony\Component\HttpFoundation\Response - */ public function toResponse($request) { $contextFactory = TransformationContextFactory::create() diff --git a/src/Contracts/ResponsableData.php b/src/Contracts/ResponsableData.php index 6549c67c..d565f980 100644 --- a/src/Contracts/ResponsableData.php +++ b/src/Contracts/ResponsableData.php @@ -2,8 +2,18 @@ namespace Spatie\LaravelData\Contracts; -interface ResponsableData +use Illuminate\Contracts\Support\Responsable; +use Symfony\Component\HttpFoundation\Response; + +interface ResponsableData extends TransformableData, Responsable { + /** + * @param \Illuminate\Http\Request $request + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function toResponse($request); + public static function allowedRequestIncludes(): ?array; public static function allowedRequestExcludes(): ?array; diff --git a/src/CursorPaginatedDataCollection.php b/src/CursorPaginatedDataCollection.php index cb8f85d1..cd53edb0 100644 --- a/src/CursorPaginatedDataCollection.php +++ b/src/CursorPaginatedDataCollection.php @@ -49,9 +49,11 @@ public function __construct( } /** - * @param Closure(TValue, TKey): TValue $through + * @template TOtherValue * - * @return static + * @param Closure(TValue, TKey): TOtherValue $through + * + * @return static */ public function through(Closure $through): static { diff --git a/src/PaginatedDataCollection.php b/src/PaginatedDataCollection.php index 5d7434b0..0785b556 100644 --- a/src/PaginatedDataCollection.php +++ b/src/PaginatedDataCollection.php @@ -48,9 +48,11 @@ public function __construct( } /** - * @param Closure(TValue, TKey): TValue $through + * @template TOtherValue * - * @return static + * @param Closure(TValue, TKey): TOtherValue $through + * + * @return static */ public function through(Closure $through): static { diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 6d6b1275..1a8b35c8 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -8,6 +8,7 @@ use Spatie\LaravelData\Attributes\Computed; use Spatie\LaravelData\Attributes\GetsCast; use Spatie\LaravelData\Attributes\Hidden; +use Spatie\LaravelData\Attributes\WithCastAndTransformer; use Spatie\LaravelData\Attributes\WithoutValidation; use Spatie\LaravelData\Attributes\WithTransformer; use Spatie\LaravelData\Casts\Cast; @@ -89,7 +90,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(), + transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer || $attribute instanceof WithCastAndTransformer)?->get(), inputMappedName: $inputMappedName, outputMappedName: $outputMappedName, attributes: $attributes, diff --git a/tests/Fakes/CastTransformers/FakeCastTransformer.php b/tests/Fakes/CastTransformers/FakeCastTransformer.php new file mode 100644 index 00000000..e46dcee9 --- /dev/null +++ b/tests/Fakes/CastTransformers/FakeCastTransformer.php @@ -0,0 +1,24 @@ +transformer)->toEqual(new DateTimeInterfaceTransformer('d-m-y')); }); +it('can get the cast with transformer attribute', function () { + $helper = resolveHelper(new class () { + #[WithCastAndTransformer(FakeCastTransformer::class)] + public SimpleData $property; + }); + + expect($helper->transformer)->toEqual(new FakeCastTransformer()); + expect($helper->cast)->toEqual(new FakeCastTransformer()); +}); + it('can get the mapped input name', function () { $helper = resolveHelper(new class () { #[MapInputName('other')] From 443439d6ef843d82878cc3f0be2ce5941bdf7b1d Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Mon, 15 Jan 2024 11:58:23 +0000 Subject: [PATCH 070/124] Fix styling --- src/Contracts/ResponsableData.php | 1 - tests/Fakes/CastTransformers/FakeCastTransformer.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Contracts/ResponsableData.php b/src/Contracts/ResponsableData.php index d565f980..1bf1d65d 100644 --- a/src/Contracts/ResponsableData.php +++ b/src/Contracts/ResponsableData.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Contracts; use Illuminate\Contracts\Support\Responsable; -use Symfony\Component\HttpFoundation\Response; interface ResponsableData extends TransformableData, Responsable { diff --git a/tests/Fakes/CastTransformers/FakeCastTransformer.php b/tests/Fakes/CastTransformers/FakeCastTransformer.php index e46dcee9..1fc80e23 100644 --- a/tests/Fakes/CastTransformers/FakeCastTransformer.php +++ b/tests/Fakes/CastTransformers/FakeCastTransformer.php @@ -11,7 +11,6 @@ class FakeCastTransformer implements Cast, Transformer { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed { return $value; From 0fc8124df21605a77210ceed6189de9fe4906561 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 15 Jan 2024 14:25:08 +0100 Subject: [PATCH 071/124] Test cleanup --- src/Concerns/DefaultableData.php | 11 - src/Contracts/DataObject.php | 2 +- src/Contracts/DefaultableData.php | 8 - src/Data.php | 1 - src/DataPipes/DefaultValuesDataPipe.php | 12 +- src/Dto.php | 5 +- src/Resource.php | 3 +- src/Support/DataClass.php | 2 - tests/AppendTest.php | 86 + tests/Casts/DateTimeInterfaceCastTest.php | 17 + tests/CreationTest.php | 909 +++++++++++ tests/DataCollectionTest.php | 193 +-- tests/DataPipelineTest.php | 28 - .../DataPipes/CastPropertiesDataPipeTest.php | 275 ---- tests/DataPipes/DefaultValuesDataPipeTest.php | 36 - tests/DataTest.php | 1426 +---------------- tests/Datasets/DataCollection.php | 11 - tests/Datasets/DataTest.php | 197 --- tests/EmptyTest.php | 54 + ...peTest.php => FillRouteParametersTest.php} | 0 tests/LivewireTest.php | 19 + ...solverTest.php => MagicalCreationTest.php} | 1 + ...ertiesDataPipeTest.php => MappingTest.php} | 121 +- tests/PartialsTest.php | 503 ++++++ tests/PipelineTest.php | 60 + .../{RequestDataTest.php => RequestTest.php} | 30 +- .../PartialsTreeFromRequestResolverTest.php | 262 --- .../VisibleDataFieldsResolverTest.php | 56 +- tests/Support/DataTypeTest.php | 2 - tests/TransformationTest.php | 371 +++++ tests/WithDataTest.php | 61 + tests/WrapTest.php | 233 +++ ...a_paginated_cursor_data_collection__1.json | 58 + ...nsform_a_paginated_data_collection__1.json | 109 ++ 34 files changed, 2656 insertions(+), 2506 deletions(-) delete mode 100644 src/Concerns/DefaultableData.php delete mode 100644 src/Contracts/DefaultableData.php create mode 100644 tests/AppendTest.php create mode 100644 tests/CreationTest.php delete mode 100644 tests/DataPipelineTest.php delete mode 100644 tests/DataPipes/CastPropertiesDataPipeTest.php delete mode 100644 tests/DataPipes/DefaultValuesDataPipeTest.php delete mode 100644 tests/Datasets/DataCollection.php delete mode 100644 tests/Datasets/DataTest.php create mode 100644 tests/EmptyTest.php rename tests/{DataPipes/FillRouteParameterPropertiesPipeTest.php => FillRouteParametersTest.php} (100%) create mode 100644 tests/LivewireTest.php rename tests/{Resolvers/DataFromSomethingResolverTest.php => MagicalCreationTest.php} (99%) rename tests/{DataPipes/MapPropertiesDataPipeTest.php => MappingTest.php} (55%) create mode 100644 tests/PipelineTest.php rename tests/{RequestDataTest.php => RequestTest.php} (82%) delete mode 100644 tests/Resolvers/PartialsTreeFromRequestResolverTest.php create mode 100644 tests/TransformationTest.php create mode 100644 tests/WithDataTest.php create mode 100644 tests/WrapTest.php create mode 100644 tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json create mode 100644 tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json diff --git a/src/Concerns/DefaultableData.php b/src/Concerns/DefaultableData.php deleted file mode 100644 index 3eea8915..00000000 --- a/src/Concerns/DefaultableData.php +++ /dev/null @@ -1,11 +0,0 @@ -defaultable - ? app()->call([$class->name, 'defaults']) - : []; - $class ->properties ->filter(fn (DataProperty $property) => ! $properties->has($property->name)) - ->each(function (DataProperty $property) use ($dataDefaults, &$properties) { - if (array_key_exists($property->name, $dataDefaults)) { - $properties[$property->name] = $dataDefaults[$property->name]; - - return; - } - + ->each(function (DataProperty $property) use (&$properties) { if ($property->hasDefaultValue) { $properties[$property->name] = $property->defaultValue; diff --git a/src/Dto.php b/src/Dto.php index 396f30fa..3e566fc5 100644 --- a/src/Dto.php +++ b/src/Dto.php @@ -3,15 +3,12 @@ namespace Spatie\LaravelData; use Spatie\LaravelData\Concerns\BaseData; -use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; -use Spatie\LaravelData\Contracts\DefaultableData as DefaultDataContract; use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; -class Dto implements ValidateableDataContract, BaseDataContract, DefaultDataContract +class Dto implements ValidateableDataContract, BaseDataContract { use ValidateableData; use BaseData; - use DefaultableData; } diff --git a/src/Resource.php b/src/Resource.php index 314a4985..47cfab6d 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -20,7 +20,7 @@ use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract, DefaultDataContract +class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract { use BaseData; use AppendableData; @@ -30,5 +30,4 @@ class Resource implements BaseDataContract, AppendableDataContract, IncludeableD use WrappableData; use EmptyData; use ContextableData; - use DefaultableData; } diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 04caef58..f337bc7c 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -44,7 +44,6 @@ public function __construct( public readonly bool $responsable, public readonly bool $transformable, public readonly bool $validateable, - public readonly bool $defaultable, public readonly bool $wrappable, public readonly bool $emptyData, public readonly Collection $attributes, @@ -107,7 +106,6 @@ public static function create(ReflectionClass $class): self responsable: $responsable, transformable: $class->implementsInterface(TransformableData::class), validateable: $class->implementsInterface(ValidateableData::class), - defaultable: $class->implementsInterface(DefaultableData::class), wrappable: $class->implementsInterface(WrappableData::class), emptyData: $class->implementsInterface(EmptyData::class), attributes: $attributes, diff --git a/tests/AppendTest.php b/tests/AppendTest.php new file mode 100644 index 00000000..d7e2b277 --- /dev/null +++ b/tests/AppendTest.php @@ -0,0 +1,86 @@ + "{$this->name} from Spatie"]; + } + }; + + expect($data->toArray())->toMatchArray([ + 'name' => 'Freek', + 'alt_name' => 'Freek from Spatie', + ]); +}); + +it('can append data via method overwriting with closures', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string $name) + { + } + + public function with(): array + { + return [ + 'alt_name' => static function (self $data) { + return $data->name.' from Spatie via closure'; + }, + ]; + } + }; + + expect($data->toArray())->toMatchArray([ + 'name' => 'Freek', + 'alt_name' => 'Freek from Spatie via closure', + ]); +}); + +it('can append data via method call', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string $name) + { + } + }; + + $transformed = $data->additional([ + 'company' => 'Spatie', + 'alt_name' => fn (Data $data) => "{$data->name} from Spatie", + ])->toArray(); + + expect($transformed)->toMatchArray([ + 'name' => 'Freek', + 'company' => 'Spatie', + 'alt_name' => 'Freek from Spatie', + ]); +}); + + +test('when using additional method and with method the additional method will be prioritized', function () { + $data = new class ('Freek') extends Data { + public function __construct(public string $name) + { + } + + public function with(): array + { + return [ + 'alt_name' => static function (self $data) { + return $data->name.' from Spatie via closure'; + }, + ]; + } + }; + + expect($data->additional(['alt_name' => 'I m Freek from additional'])->toArray())->toMatchArray([ + 'name' => 'Freek', + 'alt_name' => 'I m Freek from additional', + ]); +}); + diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 4b3e6a60..239abea9 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -3,8 +3,10 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Carbon\CarbonTimeZone; +use Spatie\LaravelData\Attributes\WithCast; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Casts\Uncastable; +use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; @@ -191,3 +193,18 @@ ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); }); + +it('can define multiple date formats to be used', function () { + $data = new class () extends Data { + public function __construct( + #[WithCast(DateTimeInterfaceCast::class, ['Y-m-d\TH:i:sP', 'Y-m-d H:i:s'])] + public ?DateTime $date = null + ) { + } + }; + + expect($data::from(['date' => '2022-05-16T14:37:56+00:00']))->toArray() + ->toMatchArray(['date' => '2022-05-16T14:37:56+00:00']) + ->and($data::from(['date' => '2022-05-16 17:00:00']))->toArray() + ->toMatchArray(['date' => '2022-05-16T17:00:00+00:00']); +}); diff --git a/tests/CreationTest.php b/tests/CreationTest.php new file mode 100644 index 00000000..3fa0cb13 --- /dev/null +++ b/tests/CreationTest.php @@ -0,0 +1,909 @@ + 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => '16-06-1994', + 'defaultCast' => '1994-05-16T12:00:00+01:00', + 'nestedData' => [ + 'string' => 'hello', + ], + 'nestedCollection' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], + 'nestedArray' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], + ]); + + expect($data)->toBeInstanceOf(ComplicatedData::class) + ->withoutType->toEqual(42) + ->int->toEqual(42) + ->bool->toBeTrue() + ->float->toEqual(3.14) + ->string->toEqual('Hello world') + ->array->toEqual([1, 1, 2, 3, 5, 8]) + ->nullable->toBeNull() + ->undefinable->toBeInstanceOf(Optional::class) + ->mixed->toEqual(42) + ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00')) + ->explicitCast->toEqual(CarbonImmutable::createFromFormat('d-m-Y', '16-06-1994')) + ->nestedData->toEqual(SimpleData::from('hello')) + ->nestedCollection->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ], DataCollection::class)) + ->nestedArray->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ])); +}); + +it("won't cast a property that is already in the correct type", function () { + $data = ComplicatedData::from([ + 'withoutType' => 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => DateTime::createFromFormat('d-m-Y', '16-06-1994'), + 'defaultCast' => DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00'), + 'nestedData' => SimpleData::from('hello'), + 'nestedCollection' => SimpleData::collect([ + 'never', 'gonna', 'give', 'you', 'up', + ], DataCollection::class), + 'nestedArray' => SimpleData::collect([ + 'never', 'gonna', 'give', 'you', 'up', + ]), + ]); + + expect($data)->toBeInstanceOf(ComplicatedData::class) + ->withoutType->toEqual(42) + ->int->toEqual(42) + ->bool->toBeTrue() + ->float->toEqual(3.14) + ->string->toEqual('Hello world') + ->array->toEqual([1, 1, 2, 3, 5, 8]) + ->nullable->toBeNull() + ->mixed->toBe(42) + ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00')) + ->explicitCast->toEqual(DateTime::createFromFormat('d-m-Y', '16-06-1994')) + ->nestedData->toEqual(SimpleData::from('hello')) + ->nestedCollection->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ], DataCollection::class)) + ->nestedArray->toEqual(SimpleData::collect([ + SimpleData::from('never'), + SimpleData::from('gonna'), + SimpleData::from('give'), + SimpleData::from('you'), + SimpleData::from('up'), + ])); +}); + +it('allows creating data objects using Lazy', function () { + $data = NestedLazyData::from([ + 'simple' => Lazy::create(fn () => SimpleData::from('Hello')), + ]); + + expect($data->simple) + ->toBeInstanceOf(Lazy::class) + ->toEqual(Lazy::create(fn () => SimpleData::from('Hello'))); +}); + +it('can set a custom cast', function () { + $dataClass = new class () extends Data { + #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')] + public DateTimeImmutable $date; + }; + + $data = $dataClass::from([ + 'date' => '2022-01-18', + ]); + + expect($data->date) + ->toBeInstanceOf(DateTimeImmutable::class) + ->toEqual(DateTimeImmutable::createFromFormat('Y-m-d', '2022-01-18')); +}); + +it('allows casting of enums', function () { + $data = EnumData::from([ + 'enum' => 'foo', + ]); + + expect($data->enum) + ->toBeInstanceOf(DummyBackedEnum::class) + ->toEqual(DummyBackedEnum::FOO); +}); + +it('can optionally create data', function () { + expect(SimpleData::optional(null))->toBeNull(); + expect(new SimpleData('Hello world'))->toEqual( + SimpleData::optional(['string' => 'Hello world']) + ); +}); + +it('can create a data model without constructor', function () { + expect(SimpleDataWithoutConstructor::fromString('Hello')) + ->toEqual(SimpleDataWithoutConstructor::from('Hello')); + + expect(SimpleDataWithoutConstructor::fromString('Hello')) + ->toEqual(SimpleDataWithoutConstructor::from([ + 'string' => 'Hello', + ])); + + expect( + new DataCollection(SimpleDataWithoutConstructor::class, [ + SimpleDataWithoutConstructor::fromString('Hello'), + SimpleDataWithoutConstructor::fromString('World'), + ]) + ) + ->toEqual(SimpleDataWithoutConstructor::collect(['Hello', 'World'], DataCollection::class)); +}); + +it('can create a data object from a model', function () { + DummyModel::migrate(); + + $model = DummyModel::create([ + 'string' => 'test', + 'boolean' => true, + 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), + 'nullable_date' => null, + ]); + + $dataClass = new class () extends Data { + public string $string; + + public bool $boolean; + + public Carbon $date; + + public ?Carbon $nullable_date; + }; + + $data = $dataClass::from(DummyModel::findOrFail($model->id)); + + expect($data) + ->string->toEqual('test') + ->boolean->toBeTrue() + ->nullable_date->toBeNull() + ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); +}); + +it('can create a data object from a stdClass object', function () { + $object = (object) [ + 'string' => 'test', + 'boolean' => true, + 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), + 'nullable_date' => null, + ]; + + $dataClass = new class () extends Data { + public string $string; + + public bool $boolean; + + public CarbonImmutable $date; + + public ?Carbon $nullable_date; + }; + + $data = $dataClass::from($object); + + expect($data) + ->string->toEqual('test') + ->boolean->toBeTrue() + ->nullable_date->toBeNull() + ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); +}); + +it('has support for readonly properties', function () { + $dataClass = new class ('') extends Data { + public function __construct( + public readonly string $string + ) { + } + }; + + $data = $dataClass::from(['string' => 'Hello world']); + + expect($data)->toBeInstanceOf($dataClass::class) + ->and($data->string)->toEqual('Hello world'); +}); + + +it('has support for intersection types', function () { + $collection = collect(['a', 'b', 'c']); + + $dataClass = new class () extends Data { + public Arrayable & \Countable $intersection; + }; + + $data = $dataClass::from(['intersection' => $collection]); + + expect($data)->toBeInstanceOf($dataClass::class) + ->and($data->intersection)->toEqual($collection); +}); + +it( + 'can construct a data object with both constructor promoted and default properties', + function () { + $dataClass = new class ('') extends Data { + public string $property; + + public function __construct( + public string $promoted_property, + ) { + } + }; + + $data = $dataClass::from([ + 'property' => 'A', + 'promoted_property' => 'B', + ]); + + expect($data) + ->property->toEqual('A') + ->promoted_property->toEqual('B'); + } +); + +it('can construct a data object with default values', function () { + $dataClass = new class ('', '') extends Data { + public string $property; + + public string $default_property = 'Hello'; + + public function __construct( + public string $promoted_property, + public string $default_promoted_property = 'Hello Again', + ) { + } + }; + + $data = $dataClass::from([ + 'property' => 'Test', + 'promoted_property' => 'Test Again', + ]); + + expect($data) + ->property->toEqual('Test') + ->promoted_property->toEqual('Test Again') + ->default_property->toEqual('Hello') + ->default_promoted_property->toEqual('Hello Again'); +}); + + +it('can construct a data object with default values and overwrite them', function () { + $dataClass = new class ('', '') extends Data { + public string $property; + + public string $default_property = 'Hello'; + + public function __construct( + public string $promoted_property, + public string $default_promoted_property = 'Hello Again', + ) { + } + }; + + $data = $dataClass::from([ + 'property' => 'Test', + 'default_property' => 'Test', + 'promoted_property' => 'Test Again', + 'default_promoted_property' => 'Test Again', + ]); + + expect($data) + ->property->toEqual('Test') + ->promoted_property->toEqual('Test Again') + ->default_property->toEqual('Test') + ->default_promoted_property->toEqual('Test Again'); +}); + +it('can manually set values in the constructor', function () { + $dataClass = new class ('', '') extends Data { + public string $member; + + public string $other_member; + + public string $member_with_default = 'default'; + + public string $member_to_set; + + public function __construct( + public string $promoted, + string $non_promoted, + string $non_promoted_with_default = 'default', + public string $promoted_with_with_default = 'default', + ) { + $this->member = "changed_in_constructor: {$non_promoted}"; + $this->other_member = "changed_in_constructor: {$non_promoted_with_default}"; + } + }; + + $data = $dataClass::from([ + 'promoted' => 'A', + 'non_promoted' => 'B', + 'non_promoted_with_default' => 'C', + 'promoted_with_with_default' => 'D', + 'member_to_set' => 'E', + 'member_with_default' => 'F', + ]); + + expect($data->toArray())->toMatchArray([ + 'member' => 'changed_in_constructor: B', + 'other_member' => 'changed_in_constructor: C', + 'member_with_default' => 'F', + 'promoted' => 'A', + 'promoted_with_with_default' => 'D', + 'member_to_set' => 'E', + ]); + + $data = $dataClass::from([ + 'promoted' => 'A', + 'non_promoted' => 'B', + 'member_to_set' => 'E', + ]); + + expect($data->toArray())->toMatchArray([ + 'member' => 'changed_in_constructor: B', + 'other_member' => 'changed_in_constructor: default', + 'member_with_default' => 'default', + 'promoted' => 'A', + 'promoted_with_with_default' => 'default', + 'member_to_set' => 'E', + ]); +}); + +it('can cast data object and collectables using a custom cast', function () { + $dataWithDefaultCastsClass = new class () extends Data { + public SimpleData $nestedData; + + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection; + }; + + $dataWithCustomCastsClass = new class () extends Data { + #[WithCast(ConfidentialDataCast::class)] + public SimpleData $nestedData; + + #[WithCast(ConfidentialDataCollectionCast::class)] + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection; + }; + + $dataWithDefaultCasts = $dataWithDefaultCastsClass::from([ + 'nestedData' => 'a secret', + 'nestedDataCollection' => ['another secret', 'yet another secret'], + ]); + + $dataWithCustomCasts = $dataWithCustomCastsClass::from([ + 'nestedData' => 'a secret', + 'nestedDataCollection' => ['another secret', 'yet another secret'], + ]); + + expect($dataWithDefaultCasts) + ->nestedData->toEqual(SimpleData::from('a secret')) + ->and($dataWithDefaultCasts) + ->nestedDataCollection->toEqual(SimpleData::collect(['another secret', 'yet another secret'])); + + expect($dataWithCustomCasts) + ->nestedData->toEqual(SimpleData::from('CONFIDENTIAL')) + ->and($dataWithCustomCasts) + ->nestedDataCollection->toEqual(SimpleData::collect(['CONFIDENTIAL', 'CONFIDENTIAL'])); +}); + +it('can create a data object with defaults by calling an empty from', function () { + $dataClass = new class ('', '', '') extends Data { + public function __construct( + public ?string $string, + public Optional|string $optionalString, + public string $stringWithDefault = 'Hi', + ) { + } + }; + + expect(new $dataClass(null, new Optional(), 'Hi')) + ->toEqual($dataClass::from([])); +}); + +it('can cast built-in types with custom casts', function () { + $dataClass = new class ('', '') extends Data { + public function __construct( + public string $without_cast, + #[WithCast(StringToUpperCast::class)] + public string $with_cast + ) { + } + }; + + $data = $dataClass::from([ + 'without_cast' => 'Hello World', + 'with_cast' => 'Hello World', + ]); + + expect($data) + ->without_cast->toEqual('Hello World') + ->with_cast->toEqual('HELLO WORLD'); +}); + +it('can cast data object with a castable property using anonymous class', function () { + $dataWithCastablePropertyClass = new class (new SimpleCastable('')) extends Data { + public function __construct( + #[WithCastable(SimpleCastable::class)] + public SimpleCastable $castableData, + ) { + } + }; + + $dataWithCastableProperty = $dataWithCastablePropertyClass::from(['castableData' => 'HELLO WORLD']); + + expect($dataWithCastableProperty) + ->castableData->toEqual(new SimpleCastable('HELLO WORLD')); +}); + +it('can assign a false value and the process will continue', function () { + $dataClass = new class () extends Data { + public bool $false; + + public bool $true; + }; + + $data = $dataClass::from([ + 'false' => false, + 'true' => true, + ]); + + expect($data) + ->false->toBeFalse() + ->true->toBeTrue(); +}); + +it('can create an partial data object using optional values', function () { + $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { + public function __construct( + public string $string, + public string|Optional $undefinable_string, + #[WithCast(StringToUpperCast::class)] + public string|Optional $undefinable_string_with_cast, + ) { + } + }; + + $partialData = $dataClass::from([ + 'string' => 'Hello World', + ]); + + expect($partialData) + ->string->toEqual('Hello World') + ->undefinable_string->toEqual(Optional::create()) + ->undefinable_string_with_cast->toEqual(Optional::create()); + + $fullData = $dataClass::from([ + 'string' => 'Hello World', + 'undefinable_string' => 'Hello World', + 'undefinable_string_with_cast' => 'Hello World', + ]); + + expect($fullData) + ->string->toEqual('Hello World') + ->undefinable_string->toEqual('Hello World') + ->undefinable_string_with_cast->toEqual('HELLO WORLD'); +}); + +it('can use context in casts based upon the properties of the data object', function () { + $dataClass = new class () extends Data { + public SimpleData $nested; + + public string $string; + + #[WithCast(ContextAwareCast::class)] + public string $casted; + }; + + $data = $dataClass::from([ + 'nested' => 'Hello', + 'string' => 'world', + 'casted' => 'json:', + ]); + + expect($data)->casted + ->toEqual('json:+{"nested":{"string":"Hello"},"string":"world","casted":"json:"}'); +}); + +it('can magically create a data object', function () { + $dataClass = new class ('', '') extends Data { + public function __construct( + public mixed $propertyA, + public mixed $propertyB, + ) { + } + + public static function fromStringWithDefault(string $a, string $b = 'World') + { + return new self($a, $b); + } + + public static function fromIntsWithDefault(int $a, int $b) + { + return new self($a, $b); + } + + public static function fromSimpleDara(SimpleData $data) + { + return new self($data->string, $data->string); + } + + public static function fromData(Data $data) + { + return new self('data', json_encode($data)); + } + }; + + expect($dataClass::from('Hello'))->toEqual(new $dataClass('Hello', 'World')) + ->and($dataClass::from('Hello', 'World'))->toEqual(new $dataClass('Hello', 'World')) + ->and($dataClass::from(42, 69))->toEqual(new $dataClass(42, 69)) + ->and($dataClass::from(SimpleData::from('Hello')))->toEqual(new $dataClass('Hello', 'Hello')) + ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); +}); + +it( + 'will throw a custom exception when a data constructor cannot be called due to missing component', + function () { + SimpleData::from([]); + } +)->throws(CannotCreateData::class, 'the constructor requires 1 parameters'); + +it('will take properties from a base class into account when creating a data object', function () { + $dataClass = new class ('') extends SimpleData { + public int $int; + }; + + $data = $dataClass::from(['string' => 'Hi', 'int' => 42]); + + expect($data) + ->string->toBe('Hi') + ->int->toBe(42); +}); + +it('can set a default value for data object which is taken into account when creating the data object', function () { + $dataObject = new class ('', '') extends Data { + #[Min(10)] + public string|Optional $full_name; + + public function __construct( + public string $first_name, + public string $last_name, + ) { + $this->full_name = "{$this->first_name} {$this->last_name}"; + } + }; + + expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Versieck'); + + expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect(fn () => $dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'too short'])) + ->toThrow(ValidationException::class); +}); + +it('can have a computed value when creating the data object', function () { + $dataObject = new class ('', '') extends Data { + #[Computed] + public string $full_name; + + public function __construct( + public string $first_name, + public string $last_name, + ) { + $this->full_name = "{$this->first_name} {$this->last_name}"; + } + }; + + expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) + ->first_name->toBe('Ruben') + ->last_name->toBe('Van Assche') + ->full_name->toBe('Ruben Van Assche'); + + expect(fn () => $dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) + ->toThrow(CannotSetComputedValue::class); +}); + +it('can have a nullable computed value', function () { + $dataObject = new class ('', '') extends Data { + #[Computed] + public ?string $upper_name; + + public function __construct( + public ?string $name, + ) { + $this->upper_name = $name ? strtoupper($name) : null; + } + }; + + expect($dataObject::from(['name' => 'Ruben'])) + ->name->toBe('Ruben') + ->upper_name->toBe('RUBEN'); + + expect($dataObject::from(['name' => null])) + ->name->toBeNull() + ->upper_name->toBeNull(); + + expect($dataObject::validateAndCreate(['name' => 'Ruben'])) + ->name->toBe('Ruben') + ->upper_name->toBe('RUBEN'); + + expect($dataObject::validateAndCreate(['name' => null])) + ->name->toBeNull() + ->upper_name->toBeNull(); + + expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => 'RUBEN'])) + ->toThrow(CannotSetComputedValue::class); + + expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => null])) + ->name->toBeNull() + ->upper_name->toBeNull(); // Case conflicts with DefaultsPipe, ignoring it for now +}); + +it('throws a readable exception message when the constructor fails', function ( + array $data, + string $message, +) { + try { + MultiData::from($data); + } catch (CannotCreateData $e) { + expect($e->getMessage())->toBe($message); + + return; + } + + throw new Exception('We should not reach this point'); +})->with(fn () => [ + 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('a can create a collection of data objects', function () { + $collectionA = new DataCollection(SimpleData::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]); + + $collectionB = SimpleData::collect([ + 'A', + 'B', + ], DataCollection::class); + + expect($collectionB)->toArray() + ->toMatchArray($collectionA->toArray()); +}); + +it('will use magic methods when creating a collection of data objects', function () { + $dataClass = new class ('') extends Data { + public function __construct(public string $otherString) + { + } + + public static function fromSimpleData(SimpleData $simpleData): static + { + return new self($simpleData->string); + } + }; + + $collection = new DataCollection($dataClass::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]); + + expect($collection[0]) + ->toBeInstanceOf($dataClass::class) + ->otherString->toEqual('A'); + + expect($collection[1]) + ->toBeInstanceOf($dataClass::class) + ->otherString->toEqual('B'); +}); + +it('can return a custom data collection when collecting data', function () { + $class = new class ('') extends Data implements DeprecatedDataContract { + use WithDeprecatedCollectionMethod; + + protected static string $_collectionClass = CustomDataCollection::class; + + public function __construct(public string $string) + { + } + }; + + $collection = $class::collection([ + ['string' => 'A'], + ['string' => 'B'], + ]); + + expect($collection)->toBeInstanceOf(CustomDataCollection::class); +}); + +it('can return a custom paginated data collection when collecting data', function () { + $class = new class ('') extends Data implements DeprecatedDataContract { + use WithDeprecatedCollectionMethod; + + protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; + + public function __construct(public string $string) + { + } + }; + + $collection = $class::collection(new LengthAwarePaginator([['string' => 'A'], ['string' => 'B']], 2, 15)); + + expect($collection)->toBeInstanceOf(CustomPaginatedDataCollection::class); +}); + +it('can magically collect data', function () { + class TestSomeCustomCollection extends Collection + { + } + + $dataClass = new class () extends Data { + public string $string; + + public static function fromString(string $string): self + { + $s = new self(); + + $s->string = $string; + + return $s; + } + + public static function collectArray(array $items): \TestSomeCustomCollection + { + return new \TestSomeCustomCollection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(\TestSomeCustomCollection::class) + ->all()->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); +}); + +it('will allow a nested data object to cast properties however it wants', function () { + $model = new DummyModel(['id' => 10]); + + $withoutModelData = NestedModelData::from([ + 'model' => ['id' => 10], + ]); + + expect($withoutModelData) + ->toBeInstanceOf(NestedModelData::class) + ->model->id->toEqual(10); + + /** @var \Spatie\LaravelData\Tests\Fakes\NestedModelData $data */ + $withModelData = NestedModelData::from([ + 'model' => $model, + ]); + + expect($withModelData) + ->toBeInstanceOf(NestedModelData::class) + ->model->id->toEqual(10); +}); + +it('will allow a nested collection object to cast properties however it wants', function () { + $data = NestedModelCollectionData::from([ + 'models' => [['id' => 10], ['id' => 20],], + ]); + + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); + + $data = NestedModelCollectionData::from([ + 'models' => [new DummyModel(['id' => 10]), new DummyModel(['id' => 20]),], + ]); + + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); + + $data = NestedModelCollectionData::from([ + 'models' => ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class), + ]); + + expect($data) + ->toBeInstanceOf(NestedModelCollectionData::class) + ->models->toEqual( + ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) + ); +}); diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 2577d2a8..71ebcfd4 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -20,55 +20,7 @@ use function Spatie\Snapshots\assertMatchesJsonSnapshot; use function Spatie\Snapshots\assertMatchesSnapshot; -it('can get a paginated data collection', function () { - $items = Collection::times(100, fn (int $index) => "Item {$index}"); - - $paginator = new LengthAwarePaginator( - $items->forPage(1, 15), - 100, - 15 - ); - - $collection = new PaginatedDataCollection(SimpleData::class, $paginator); - - expect($collection)->toBeInstanceOf(PaginatedDataCollection::class); - assertMatchesJsonSnapshot($collection->toJson()); -}); - -it('can get a paginated cursor data collection', function () { - $items = Collection::times(100, fn (int $index) => "Item {$index}"); - - $paginator = new CursorPaginator( - $items, - 15, - ); - - $collection = new CursorPaginatedDataCollection(SimpleData::class, $paginator); - - if (version_compare(app()->version(), '9.0.0', '<=')) { - $this->markTestIncomplete('Laravel 8 uses a different format'); - } - - expect($collection)->toBeInstanceOf(CursorPaginatedDataCollection::class); - assertMatchesJsonSnapshot($collection->toJson()); -}); - -test('a collection can be constructed with data object', function () { - $collectionA = new DataCollection(SimpleData::class, [ - SimpleData::from('A'), - SimpleData::from('B'), - ]); - - $collectionB = SimpleData::collect([ - 'A', - 'B', - ], DataCollection::class); - - expect($collectionB)->toArray() - ->toMatchArray($collectionA->toArray()); -}); - -test('a collection can be filtered', function () { +it('can filter a collection', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->filter(fn (SimpleData $data) => $data->string === 'A')->toArray(); @@ -79,7 +31,7 @@ ->toMatchArray($filtered); }); -test('a collection can be rejected', function () { +it('can reject items within a collection', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); $filtered = $collection->reject(fn (SimpleData $data) => $data->string === 'B')->toArray(); @@ -90,18 +42,8 @@ ->toMatchArray($filtered); }); -test('a collection can be transformed', function () { - $collection = new DataCollection(SimpleData::class, ['A', 'B']); - - $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); - - expect($filtered)->toMatchArray([ - ['string' => 'Ax'], - ['string' => 'Bx'], - ]); -}); -test('a paginated collection can be transformed', function () { +it('it can put items through a paginated data collection', function () { $collection = new PaginatedDataCollection( SimpleData::class, new LengthAwarePaginator(['A', 'B'], 2, 15), @@ -177,8 +119,7 @@ expect($collection)->toHaveCount(4); }); - -it('can update data properties withing a collection', function () { +it('can update data properties within a collection', function () { LazyData::setAllowedIncludes(null); $collection = new DataCollection(LazyData::class, [ @@ -213,7 +154,7 @@ ]); }); -it('supports lazy collections', function () { +it('can create a data collection from a Lazy Collection', function () { $lazyCollection = new LazyCollection(function () { $items = [ 'Never gonna give you up!', @@ -257,40 +198,6 @@ ); }); -test('a collection can be transformed to JSON', function () { - $collection = (new DataCollection(SimpleData::class, ['A', 'B', 'C'])); - - expect('[{"string":"A"},{"string":"B"},{"string":"C"}]') - ->toEqual($collection->toJson()) - ->toEqual(json_encode($collection)); -}); - -it('will cast data object into the data collection objects', function () { - $dataClass = new class ('') extends Data { - public function __construct(public string $otherString) - { - } - - public static function fromSimpleData(SimpleData $simpleData): static - { - return new self($simpleData->string); - } - }; - - $collection = new DataCollection($dataClass::class, [ - SimpleData::from('A'), - SimpleData::from('B'), - ]); - - expect($collection[0]) - ->toBeInstanceOf($dataClass::class) - ->otherString->toEqual('A'); - - expect($collection[1]) - ->toBeInstanceOf($dataClass::class) - ->otherString->toEqual('B'); -}); - it('can reset the keys', function () { $collection = new DataCollection(SimpleData::class, [ 1 => SimpleData::from('a'), @@ -305,63 +212,6 @@ public static function fromSimpleData(SimpleData $simpleData): static )->toEqual($collection->values()); }); -it('can use magical creation methods to create a collection', function () { - $collection = new DataCollection(SimpleData::class, ['A', 'B']); - - expect($collection->toCollection()->all()) - ->toMatchArray([ - SimpleData::from('A'), - SimpleData::from('B'), - ]); -}); - -it('can return a custom data collection when collecting data', function () { - $class = new class ('') extends Data implements DeprecatedDataContract { - use WithDeprecatedCollectionMethod; - - protected static string $_collectionClass = CustomDataCollection::class; - - public function __construct(public string $string) - { - } - }; - - $collection = $class::collection([ - ['string' => 'A'], - ['string' => 'B'], - ]); - - expect($collection)->toBeInstanceOf(CustomDataCollection::class); -}); - -it('can return a custom paginated data collection when collecting data', function () { - $class = new class ('') extends Data implements DeprecatedDataContract { - use WithDeprecatedCollectionMethod; - - protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; - - public function __construct(public string $string) - { - } - }; - - $collection = $class::collection(new LengthAwarePaginator([['string' => 'A'], ['string' => 'B']], 2, 15)); - - expect($collection)->toBeInstanceOf(CustomPaginatedDataCollection::class); -}); - -it( - 'can perform some collection operations', - function (string $operation, array $arguments, array $expected) { - $collection = new DataCollection(SimpleData::class, ['A', 'B', 'C']); - - $changedCollection = $collection->{$operation}(...$arguments); - - expect($changedCollection->toArray()) - ->toEqual($expected); - } -)->with('collection-operations'); - it('can return a sole data object', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); @@ -380,7 +230,7 @@ function (string $operation, array $arguments, array $expected) { ->toEqual($filtered->string); }); -test('a collection can be merged', function () { +it('a collection can be merged', function () { $collectionA = SimpleData::collect(collect(['A', 'B'])); $collectionB = SimpleData::collect(collect(['C', 'D'])); @@ -435,34 +285,3 @@ function (string $operation, array $arguments, array $expected) { expect($collection[1])->toBeInstanceOf(SimpleData::class); }); -it('can magically collect data', function () { - class TestSomeCustomCollection extends Collection - { - } - - $dataClass = new class () extends Data { - public string $string; - - public static function fromString(string $string): self - { - $s = new self(); - - $s->string = $string; - - return $s; - } - - public static function collectArray(array $items): \TestSomeCustomCollection - { - return new \TestSomeCustomCollection($items); - } - }; - - expect($dataClass::collect(['a', 'b', 'c'])) - ->toBeInstanceOf(\TestSomeCustomCollection::class) - ->all()->toEqual([ - $dataClass::from('a'), - $dataClass::from('b'), - $dataClass::from('c'), - ]); -}); diff --git a/tests/DataPipelineTest.php b/tests/DataPipelineTest.php deleted file mode 100644 index c64d1ce8..00000000 --- a/tests/DataPipelineTest.php +++ /dev/null @@ -1,28 +0,0 @@ -through(DefaultValuesDataPipe::class) - ->through(CastPropertiesDataPipe::class) - ->firstThrough(AuthorizedDataPipe::class); - - $reflectionProperty = tap( - new ReflectionProperty(DataPipeline::class, 'pipes'), - static fn (ReflectionProperty $r) => $r->setAccessible(true), - ); - - $pipes = $reflectionProperty->getValue($pipeline); - - expect($pipes) - ->toHaveCount(3) - ->toMatchArray([ - AuthorizedDataPipe::class, - DefaultValuesDataPipe::class, - CastPropertiesDataPipe::class, - ]); -}); diff --git a/tests/DataPipes/CastPropertiesDataPipeTest.php b/tests/DataPipes/CastPropertiesDataPipeTest.php deleted file mode 100644 index 193ddc56..00000000 --- a/tests/DataPipes/CastPropertiesDataPipeTest.php +++ /dev/null @@ -1,275 +0,0 @@ -pipe = resolve(CastPropertiesDataPipe::class); -}); - -it('maps default types', function () { - $data = ComplicatedData::from([ - 'withoutType' => 42, - 'int' => 42, - 'bool' => true, - 'float' => 3.14, - 'string' => 'Hello world', - 'array' => [1, 1, 2, 3, 5, 8], - 'nullable' => null, - 'mixed' => 42, - 'explicitCast' => '16-06-1994', - 'defaultCast' => '1994-05-16T12:00:00+01:00', - 'nestedData' => [ - 'string' => 'hello', - ], - 'nestedCollection' => [ - ['string' => 'never'], - ['string' => 'gonna'], - ['string' => 'give'], - ['string' => 'you'], - ['string' => 'up'], - ], - 'nestedArray' => [ - ['string' => 'never'], - ['string' => 'gonna'], - ['string' => 'give'], - ['string' => 'you'], - ['string' => 'up'], - ], - ]); - - expect($data)->toBeInstanceOf(ComplicatedData::class) - ->withoutType->toEqual(42) - ->int->toEqual(42) - ->bool->toBeTrue() - ->float->toEqual(3.14) - ->string->toEqual('Hello world') - ->array->toEqual([1, 1, 2, 3, 5, 8]) - ->nullable->toBeNull() - ->undefinable->toBeInstanceOf(Optional::class) - ->mixed->toEqual(42) - ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+01:00')) - ->explicitCast->toEqual(CarbonImmutable::createFromFormat('d-m-Y', '16-06-1994')) - ->nestedData->toEqual(SimpleData::from('hello')) - ->nestedCollection->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ], DataCollection::class)) - ->nestedArray->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ])); -}); - -it("won't cast a property that is already in the correct type", function () { - $data = ComplicatedData::from([ - 'withoutType' => 42, - 'int' => 42, - 'bool' => true, - 'float' => 3.14, - 'string' => 'Hello world', - 'array' => [1, 1, 2, 3, 5, 8], - 'nullable' => null, - 'mixed' => 42, - 'explicitCast' => DateTime::createFromFormat('d-m-Y', '16-06-1994'), - 'defaultCast' => DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00'), - 'nestedData' => SimpleData::from('hello'), - 'nestedCollection' => SimpleData::collect([ - 'never', 'gonna', 'give', 'you', 'up', - ], DataCollection::class), - 'nestedArray' => SimpleData::collect([ - 'never', 'gonna', 'give', 'you', 'up', - ]), - ]); - - expect($data)->toBeInstanceOf(ComplicatedData::class) - ->withoutType->toEqual(42) - ->int->toEqual(42) - ->bool->toBeTrue() - ->float->toEqual(3.14) - ->string->toEqual('Hello world') - ->array->toEqual([1, 1, 2, 3, 5, 8]) - ->nullable->toBeNull() - ->mixed->toBe(42) - ->defaultCast->toEqual(DateTime::createFromFormat(DATE_ATOM, '1994-05-16T12:00:00+02:00')) - ->explicitCast->toEqual(DateTime::createFromFormat('d-m-Y', '16-06-1994')) - ->nestedData->toEqual(SimpleData::from('hello')) - ->nestedCollection->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ], DataCollection::class)) - ->nestedArray->toEqual(SimpleData::collect([ - SimpleData::from('never'), - SimpleData::from('gonna'), - SimpleData::from('give'), - SimpleData::from('you'), - SimpleData::from('up'), - ])); -}); - -it('will allow a nested data object to handle their own types', function () { - $model = new DummyModel(['id' => 10]); - - $withoutModelData = NestedModelData::from([ - 'model' => ['id' => 10], - ]); - - expect($withoutModelData) - ->toBeInstanceOf(NestedModelData::class) - ->model->id->toEqual(10); - - /** @var \Spatie\LaravelData\Tests\Fakes\NestedModelData $data */ - $withModelData = NestedModelData::from([ - 'model' => $model, - ]); - - expect($withModelData) - ->toBeInstanceOf(NestedModelData::class) - ->model->id->toEqual(10); -}); - -it('will allow a nested collection object to handle its own types', function () { - $data = NestedModelCollectionData::from([ - 'models' => [['id' => 10], ['id' => 20],], - ]); - - expect($data) - ->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual( - ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) - ); - - $data = NestedModelCollectionData::from([ - 'models' => [new DummyModel(['id' => 10]), new DummyModel(['id' => 20]),], - ]); - - expect($data) - ->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual( - ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) - ); - - $data = NestedModelCollectionData::from([ - 'models' => ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class), - ]); - - expect($data) - ->toBeInstanceOf(NestedModelCollectionData::class) - ->models->toEqual( - ModelData::collect([['id' => 10], ['id' => 20]], DataCollection::class) - ); -}); - -it('works nicely with lazy data', function () { - $data = NestedLazyData::from([ - 'simple' => Lazy::create(fn () => SimpleData::from('Hello')), - ]); - - expect($data->simple) - ->toBeInstanceOf(Lazy::class) - ->toEqual(Lazy::create(fn () => SimpleData::from('Hello'))); -}); - -it('allows casting', function () { - $dataClass = new class () extends Data { - #[WithCast(DateTimeInterfaceCast::class, format: 'Y-m-d')] - public DateTimeImmutable $date; - }; - - $data = $dataClass::from([ - 'date' => '2022-01-18', - ]); - - expect($data->date) - ->toBeInstanceOf(DateTimeImmutable::class) - ->toEqual(DateTimeImmutable::createFromFormat('Y-m-d', '2022-01-18')); -}); - -it('allows casting of enums', function () { - $data = EnumData::from([ - 'enum' => 'foo', - ]); - - expect($data->enum) - ->toBeInstanceOf(DummyBackedEnum::class) - ->toEqual(DummyBackedEnum::FOO); -}); - -it('can manually set values in the constructor', function () { - $dataClass = new class ('', '') extends Data { - public string $member; - - public string $other_member; - - public string $member_with_default = 'default'; - - public string $member_to_set; - - public function __construct( - public string $promoted, - string $non_promoted, - string $non_promoted_with_default = 'default', - public string $promoted_with_with_default = 'default', - ) { - $this->member = "changed_in_constructor: {$non_promoted}"; - $this->other_member = "changed_in_constructor: {$non_promoted_with_default}"; - } - }; - - $data = $dataClass::from([ - 'promoted' => 'A', - 'non_promoted' => 'B', - 'non_promoted_with_default' => 'C', - 'promoted_with_with_default' => 'D', - 'member_to_set' => 'E', - 'member_with_default' => 'F', - ]); - - expect($data->toArray())->toMatchArray([ - 'member' => 'changed_in_constructor: B', - 'other_member' => 'changed_in_constructor: C', - 'member_with_default' => 'F', - 'promoted' => 'A', - 'promoted_with_with_default' => 'D', - 'member_to_set' => 'E', - ]); - - $data = $dataClass::from([ - 'promoted' => 'A', - 'non_promoted' => 'B', - 'member_to_set' => 'E', - ]); - - expect($data->toArray())->toMatchArray([ - 'member' => 'changed_in_constructor: B', - 'other_member' => 'changed_in_constructor: default', - 'member_with_default' => 'default', - 'promoted' => 'A', - 'promoted_with_with_default' => 'default', - 'member_to_set' => 'E', - ]); -}); diff --git a/tests/DataPipes/DefaultValuesDataPipeTest.php b/tests/DataPipes/DefaultValuesDataPipeTest.php deleted file mode 100644 index 2097a1be..00000000 --- a/tests/DataPipes/DefaultValuesDataPipeTest.php +++ /dev/null @@ -1,36 +0,0 @@ -toEqual($dataClass::from([])); -}); - -it('can create a data object with defined defaults', function () { - $dataClass = new class ('', '', '') extends Data { - public function __construct( - public string $stringWithDefault, - ) { - } - - public static function defaults(): array - { - return [ - 'stringWithDefault' => 'Hi', - ]; - } - }; - - expect(new $dataClass('Hi'))->toEqual($dataClass::from([])); -}); diff --git a/tests/DataTest.php b/tests/DataTest.php index d1794b98..31042b02 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -19,7 +19,7 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; -use Spatie\LaravelData\Concerns\DefaultableData; +use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; use Spatie\LaravelData\Concerns\TransformableData; @@ -67,1160 +67,7 @@ use function Spatie\Snapshots\assertMatchesSnapshot; -it('can create a resource', function () { - $data = new SimpleData('Ruben'); - - expect($data->toArray())->toMatchArray([ - 'string' => 'Ruben', - ]); -}); - -it('can create a collection of resources', function () { - $collection = SimpleData::collect(collect([ - 'Ruben', - 'Freek', - 'Brent', - ]), DataCollection::class); - - expect($collection->toArray()) - ->toMatchArray([ - ['string' => 'Ruben'], - ['string' => 'Freek'], - ['string' => 'Brent'], - ]); -}); - -it('can get the empty version of a data object', function () { - $dataClass = new class () extends Data { - public string $property; - - public string|Lazy $lazyProperty; - - public array $array; - - public Collection $collection; - - #[DataCollectionOf(SimpleData::class)] - public DataCollection $dataCollection; - - public SimpleData $data; - - public Lazy|SimpleData $lazyData; - - public bool $defaultProperty = true; - }; - - expect($dataClass::empty())->toMatchArray([ - 'property' => null, - 'lazyProperty' => null, - 'array' => [], - 'collection' => [], - 'dataCollection' => [], - 'data' => [ - 'string' => null, - ], - 'lazyData' => [ - 'string' => null, - ], - 'defaultProperty' => true, - ]); -}); - -it('can overwrite properties in an empty version of a data object', function () { - expect(SimpleData::empty())->toMatchArray([ - 'string' => null, - ]); - - expect(SimpleData::empty(['string' => 'Ruben']))->toMatchArray([ - 'string' => 'Ruben', - ]); -}); - -it('will use transformers to convert specific types', function () { - $date = new DateTime('16 may 1994'); - - $data = new class ($date) extends Data { - public function __construct(public DateTime $date) - { - } - }; - - expect($data->toArray())->toMatchArray(['date' => '1994-05-16T00:00:00+00:00']); -}); - -it('can manually specific a transformer', function () { - $date = new DateTime('16 may 1994'); - - $data = new class ($date) extends Data { - public function __construct( - #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] - public $date - ) { - } - }; - - expect($data->toArray())->toMatchArray(['date' => '16-05-1994']); -}); - -test('a transformer will never handle a null value', function () { - $data = new class (null) extends Data { - public function __construct( - #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] - public $date - ) { - } - }; - - expect($data->toArray())->toMatchArray(['date' => null]); -}); - -it('can get the data object without transforming', function () { - $data = new class ( - $dataObject = new SimpleData('Test'), - $dataCollection = new DataCollection(SimpleData::class, ['A', 'B']), - Lazy::create(fn () => new SimpleData('Lazy')), - 'Test', - $transformable = new DateTime('16 may 1994') - ) extends Data { - public function __construct( - public SimpleData $data, - #[DataCollectionOf(SimpleData::class)] - public DataCollection $dataCollection, - public Lazy|Data $lazy, - public string $string, - public DateTime $transformable - ) { - } - }; - - expect($data->all())->toMatchArray([ - 'data' => $dataObject, - 'dataCollection' => $dataCollection, - 'string' => 'Test', - 'transformable' => $transformable, - ]); - - expect($data->include('lazy')->all())->toMatchArray([ - 'data' => $dataObject, - 'dataCollection' => $dataCollection, - 'lazy' => (new SimpleData('Lazy')), - 'string' => 'Test', - 'transformable' => $transformable, - ]); -}); - -it('can append data via method overwriting', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - - public function with(): array - { - return ['alt_name' => "{$this->name} from Spatie"]; - } - }; - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - 'alt_name' => 'Freek from Spatie', - ]); -}); - -it('can append data via method overwriting with closures', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - - public function with(): array - { - return [ - 'alt_name' => static function (self $data) { - return $data->name.' from Spatie via closure'; - }, - ]; - } - }; - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - 'alt_name' => 'Freek from Spatie via closure', - ]); -}); - -test('when using additional method and with method additional method gets priority', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - - public function with(): array - { - return [ - 'alt_name' => static function (self $data) { - return $data->name.' from Spatie via closure'; - }, - ]; - } - }; - - expect($data->additional(['alt_name' => 'I m Freek from additional'])->toArray())->toMatchArray([ - 'name' => 'Freek', - 'alt_name' => 'I m Freek from additional', - ]); -}); - -it('can get the data object without mapping properties names', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - - expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) - ->toMatchArray([ - 'camelName' => 'Freek', - ]); -}); - -it('can get the data object without mapping', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - - expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) - ->toMatchArray([ - 'camelName' => 'Freek', - ]); -}); - -it('can get the data object with mapping properties by default', function () { - $data = new class ('Freek') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName - ) { - } - }; - expect($data->transform())->toMatchArray([ - 'snake_name' => 'Freek', - ]); -}); - -it('can get the data object with mapping properties names', function () { - $data = new class ('Freek', 'Hello World') extends Data { - public function __construct( - #[MapOutputName('snake_name')] - public string $camelName, - public string $helloCamelName - ) { - } - }; - - expect($data->toArray())->toMatchArray([ - 'snake_name' => 'Freek', - 'helloCamelName' => 'Hello World', - ]); -}); - -it('can append data via method call', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string $name) - { - } - }; - - $transformed = $data->additional([ - 'company' => 'Spatie', - 'alt_name' => fn (Data $data) => "{$data->name} from Spatie", - ])->toArray(); - - expect($transformed)->toMatchArray([ - 'name' => 'Freek', - 'company' => 'Spatie', - 'alt_name' => 'Freek from Spatie', - ]); -}); - -it('can optionally create data', function () { - expect(SimpleData::optional(null))->toBeNull(); - expect(new SimpleData('Hello world'))->toEqual( - SimpleData::optional(['string' => 'Hello world']) - ); -}); - -it('can create a data model without constructor', function () { - expect(SimpleDataWithoutConstructor::fromString('Hello')) - ->toEqual(SimpleDataWithoutConstructor::from('Hello')); - - expect(SimpleDataWithoutConstructor::fromString('Hello')) - ->toEqual(SimpleDataWithoutConstructor::from([ - 'string' => 'Hello', - ])); - - expect( - new DataCollection(SimpleDataWithoutConstructor::class, [ - SimpleDataWithoutConstructor::fromString('Hello'), - SimpleDataWithoutConstructor::fromString('World'), - ]) - ) - ->toEqual(SimpleDataWithoutConstructor::collect(['Hello', 'World'], DataCollection::class)); -}); - -it('can create a data object from a model', function () { - DummyModel::migrate(); - - $model = DummyModel::create([ - 'string' => 'test', - 'boolean' => true, - 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), - 'nullable_date' => null, - ]); - - $dataClass = new class () extends Data { - public string $string; - - public bool $boolean; - - public Carbon $date; - - public ?Carbon $nullable_date; - }; - - $data = $dataClass::from(DummyModel::findOrFail($model->id)); - - expect($data) - ->string->toEqual('test') - ->boolean->toBeTrue() - ->nullable_date->toBeNull() - ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); -}); - -it('can create a data object from a stdClass object', function () { - $object = (object) [ - 'string' => 'test', - 'boolean' => true, - 'date' => CarbonImmutable::create(2020, 05, 16, 12, 00, 00), - 'nullable_date' => null, - ]; - - $dataClass = new class () extends Data { - public string $string; - - public bool $boolean; - - public CarbonImmutable $date; - - public ?Carbon $nullable_date; - }; - - $data = $dataClass::from($object); - - expect($data) - ->string->toEqual('test') - ->boolean->toBeTrue() - ->nullable_date->toBeNull() - ->and(CarbonImmutable::create(2020, 05, 16, 12, 00, 00)->eq($data->date))->toBeTrue(); -}); - -it('can add the WithData trait to a request', function () { - $formRequest = new class () extends FormRequest { - use WithData; - - public string $dataClass = SimpleData::class; - }; - - $formRequest->replace([ - 'string' => 'Hello World', - ]); - - $data = $formRequest->getData(); - - expect($data)->toEqual(SimpleData::from('Hello World')); -}); - -it('can add the WithData trait to a model', function () { - $model = new class () extends Model { - use WithData; - - protected string $dataClass = SimpleData::class; - }; - - $model->fill([ - 'string' => 'Hello World', - ]); - - $data = $model->getData(); - - expect($data)->toEqual(SimpleData::from('Hello World')); -}); - -it('can define the WithData trait data class by method', function () { - $arrayable = new class () implements Arrayable { - use WithData; - - public function toArray() - { - return [ - 'string' => 'Hello World', - ]; - } - - protected function dataClass(): string - { - return SimpleData::class; - } - }; - - $data = $arrayable->getData(); - - expect($data)->toEqual(SimpleData::from('Hello World')); -}); - -it('has support fro readonly properties', function () { - $dataClass = new class ('') extends Data { - public function __construct( - public readonly string $string - ) { - } - }; - - $data = $dataClass::from(['string' => 'Hello world']); - - expect($data)->toBeInstanceOf($dataClass::class) - ->and($data->string)->toEqual('Hello world'); -}); - -it('has support for intersection types', function () { - $collection = collect(['a', 'b', 'c']); - - $dataClass = new class () extends Data { - public Arrayable & \Countable $intersection; - }; - - $data = $dataClass::from(['intersection' => $collection]); - - expect($data)->toBeInstanceOf($dataClass::class) - ->and($data->intersection)->toEqual($collection); -}); - -it('can transform to JSON', function () { - expect('{"string":"Hello"}') - ->toEqual(SimpleData::from('Hello')->toJson()) - ->toEqual(json_encode(SimpleData::from('Hello'))); -}); - -it( - 'can construct a data object with both constructor promoted and default properties', - function () { - $dataClass = new class ('') extends Data { - public string $property; - - public function __construct( - public string $promoted_property, - ) { - } - }; - - $data = $dataClass::from([ - 'property' => 'A', - 'promoted_property' => 'B', - ]); - - expect($data) - ->property->toEqual('A') - ->promoted_property->toEqual('B'); - } -); - -it('can construct a data object with default values', function () { - $dataClass = new class ('', '') extends Data { - public string $property; - - public string $default_property = 'Hello'; - - public function __construct( - public string $promoted_property, - public string $default_promoted_property = 'Hello Again', - ) { - } - }; - - $data = $dataClass::from([ - 'property' => 'Test', - 'promoted_property' => 'Test Again', - ]); - - expect($data) - ->property->toEqual('Test') - ->promoted_property->toEqual('Test Again') - ->default_property->toEqual('Hello') - ->default_promoted_property->toEqual('Hello Again'); -}); - -it('can construct a data object with default values and overwrite them', function () { - $dataClass = new class ('', '') extends Data { - public string $property; - - public string $default_property = 'Hello'; - - public function __construct( - public string $promoted_property, - public string $default_promoted_property = 'Hello Again', - ) { - } - }; - - $data = $dataClass::from([ - 'property' => 'Test', - 'default_property' => 'Test', - 'promoted_property' => 'Test Again', - 'default_promoted_property' => 'Test Again', - ]); - - expect($data) - ->property->toEqual('Test') - ->promoted_property->toEqual('Test Again') - ->default_property->toEqual('Test') - ->default_promoted_property->toEqual('Test Again'); -}); - -it('can use a custom transformer', function () { - $nestedData = new class (42, 'Hello World') extends Data { - public function __construct( - public int $integer, - public string $string, - ) { - } - }; - - $nestedDataCollection = $nestedData::collect([ - ['integer' => 314, 'string' => 'pi'], - ['integer' => '69', 'string' => 'Laravel after hours'], - ]); - - $dataWithDefaultTransformers = new class ($nestedData, $nestedDataCollection) extends Data { - public function __construct( - public Data $nestedData, - #[DataCollectionOf(SimpleData::class)] - public array $nestedDataCollection, - ) { - } - }; - - $dataWithSpecificTransformers = new class ($nestedData, $nestedDataCollection) extends Data { - public function __construct( - #[WithTransformer(ConfidentialDataTransformer::class)] - public Data $nestedData, - #[ - WithTransformer(ConfidentialDataCollectionTransformer::class), - DataCollectionOf(SimpleData::class) - ] - public array $nestedDataCollection, - ) { - } - }; - - expect($dataWithDefaultTransformers->toArray()) - ->toMatchArray([ - 'nestedData' => ['integer' => 42, 'string' => 'Hello World'], - 'nestedDataCollection' => [ - ['integer' => 314, 'string' => 'pi'], - ['integer' => '69', 'string' => 'Laravel after hours'], - ], - ]); - - expect($dataWithSpecificTransformers->toArray()) - ->toMatchArray([ - 'nestedData' => ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], - 'nestedDataCollection' => [ - ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], - ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], - ], - ]); -}); - -it('can transform built it types with custom transformers', function () { - $data = new class ('Hello World', 'Hello World') extends Data { - public function __construct( - public string $without_transformer, - #[WithTransformer(StringToUpperTransformer::class)] - public string $with_transformer - ) { - } - }; - - expect($data->toArray())->toMatchArray([ - 'without_transformer' => 'Hello World', - 'with_transformer' => 'HELLO WORLD', - ]); -}); - -it('can cast data object and collections using a custom cast', function () { - $dataWithDefaultCastsClass = new class () extends Data { - public SimpleData $nestedData; - - #[DataCollectionOf(SimpleData::class)] - public array $nestedDataCollection; - }; - - $dataWithCustomCastsClass = new class () extends Data { - #[WithCast(ConfidentialDataCast::class)] - public SimpleData $nestedData; - - #[WithCast(ConfidentialDataCollectionCast::class)] - #[DataCollectionOf(SimpleData::class)] - public array $nestedDataCollection; - }; - - $dataWithDefaultCasts = $dataWithDefaultCastsClass::from([ - 'nestedData' => 'a secret', - 'nestedDataCollection' => ['another secret', 'yet another secret'], - ]); - - $dataWithCustomCasts = $dataWithCustomCastsClass::from([ - 'nestedData' => 'a secret', - 'nestedDataCollection' => ['another secret', 'yet another secret'], - ]); - - expect($dataWithDefaultCasts) - ->nestedData->toEqual(SimpleData::from('a secret')) - ->and($dataWithDefaultCasts) - ->nestedDataCollection->toEqual(SimpleData::collect(['another secret', 'yet another secret'])); - - expect($dataWithCustomCasts) - ->nestedData->toEqual(SimpleData::from('CONFIDENTIAL')) - ->and($dataWithCustomCasts) - ->nestedDataCollection->toEqual(SimpleData::collect(['CONFIDENTIAL', 'CONFIDENTIAL'])); -}); - -it('can cast data object with a castable property using anonymous class', function () { - $dataWithCastablePropertyClass = new class (new SimpleCastable('')) extends Data { - public function __construct( - #[WithCastable(SimpleCastable::class)] - public SimpleCastable $castableData, - ) { - } - }; - - $dataWithCastableProperty = $dataWithCastablePropertyClass::from(['castableData' => 'HELLO WORLD']); - - expect($dataWithCastableProperty) - ->castableData->toEqual(new SimpleCastable('HELLO WORLD')); -}); - -it('can cast built-in types with custom casts', function () { - $dataClass = new class ('', '') extends Data { - public function __construct( - public string $without_cast, - #[WithCast(StringToUpperCast::class)] - public string $with_cast - ) { - } - }; - - $data = $dataClass::from([ - 'without_cast' => 'Hello World', - 'with_cast' => 'Hello World', - ]); - - expect($data) - ->without_cast->toEqual('Hello World') - ->with_cast->toEqual('HELLO WORLD'); -}); - -it('continues value assignment after a false boolean', function () { - $dataClass = new class () extends Data { - public bool $false; - - public bool $true; - - public string $string; - - public Carbon $date; - }; - - $data = $dataClass::from([ - 'false' => false, - 'true' => true, - 'string' => 'string', - 'date' => Carbon::create(2020, 05, 16, 12, 00, 00), - ]); - - expect($data) - ->false->toBeFalse() - ->true->toBeTrue() - ->string->toEqual('string') - ->and(Carbon::create(2020, 05, 16, 12, 00, 00)->equalTo($data->date))->toBeTrue(); -}); - -it('can create an partial data object', function () { - $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { - public function __construct( - public string $string, - public string|Optional $undefinable_string, - #[WithCast(StringToUpperCast::class)] - public string|Optional $undefinable_string_with_cast, - ) { - } - }; - - $partialData = $dataClass::from([ - 'string' => 'Hello World', - ]); - - expect($partialData) - ->string->toEqual('Hello World') - ->undefinable_string->toEqual(Optional::create()) - ->undefinable_string_with_cast->toEqual(Optional::create()); - - $fullData = $dataClass::from([ - 'string' => 'Hello World', - 'undefinable_string' => 'Hello World', - 'undefinable_string_with_cast' => 'Hello World', - ]); - - expect($fullData) - ->string->toEqual('Hello World') - ->undefinable_string->toEqual('Hello World') - ->undefinable_string_with_cast->toEqual('HELLO WORLD'); -}); - -it('can transform a partial object', function () { - $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { - public function __construct( - public string $string, - public string|Optional $undefinable_string, - #[WithTransformer(StringToUpperTransformer::class)] - public string|Optional $undefinable_string_with_transformer, - ) { - } - }; - - $partialData = $dataClass::from([ - 'string' => 'Hello World', - ]); - - $fullData = $dataClass::from([ - 'string' => 'Hello World', - 'undefinable_string' => 'Hello World', - 'undefinable_string_with_transformer' => 'Hello World', - ]); - - expect($partialData->toArray())->toMatchArray([ - 'string' => 'Hello World', - ]); - - expect($fullData->toArray())->toMatchArray([ - 'string' => 'Hello World', - 'undefinable_string' => 'Hello World', - 'undefinable_string_with_transformer' => 'HELLO WORLD', - ]); -}); - -it('can map transformed property names', function () { - $data = new SimpleDataWithMappedProperty('hello'); - $dataCollection = SimpleDataWithMappedProperty::collect([ - ['description' => 'never'], - ['description' => 'gonna'], - ['description' => 'give'], - ['description' => 'you'], - ['description' => 'up'], - ]); - - $dataClass = new class ('hello', $data, $data, $dataCollection, $dataCollection) extends Data { - public function __construct( - #[MapOutputName('property')] - public string $string, - public SimpleDataWithMappedProperty $nested, - #[MapOutputName('nested_other')] - public SimpleDataWithMappedProperty $nested_renamed, - #[DataCollectionOf(SimpleDataWithMappedProperty::class)] - public array $nested_collection, - #[ - MapOutputName('nested_other_collection'), - DataCollectionOf(SimpleDataWithMappedProperty::class) - ] - public array $nested_renamed_collection, - ) { - } - }; - - expect($dataClass->toArray())->toMatchArray([ - 'property' => 'hello', - 'nested' => [ - 'description' => 'hello', - ], - 'nested_other' => [ - 'description' => 'hello', - ], - 'nested_collection' => [ - ['description' => 'never'], - ['description' => 'gonna'], - ['description' => 'give'], - ['description' => 'you'], - ['description' => 'up'], - ], - 'nested_other_collection' => [ - ['description' => 'never'], - ['description' => 'gonna'], - ['description' => 'give'], - ['description' => 'you'], - ['description' => 'up'], - ], - ]); -}); - -it('can map transformed properties from a complete class', function () { - $data = DataWithMapper::from([ - 'cased_property' => 'We are the knights who say, ni!', - 'data_cased_property' => - ['string' => 'Bring us a, shrubbery!'], - 'data_collection_cased_property' => [ - ['string' => 'One that looks nice!'], - ['string' => 'But not too expensive!'], - ], - ]); - - expect($data->toArray())->toMatchArray([ - 'cased_property' => 'We are the knights who say, ni!', - 'data_cased_property' => - ['string' => 'Bring us a, shrubbery!'], - 'data_collection_cased_property' => [ - ['string' => 'One that looks nice!'], - ['string' => 'But not too expensive!'], - ], - ]); -}); - -it('can use context in casts based upon the properties of the data object', function () { - $dataClass = new class () extends Data { - public SimpleData $nested; - - public string $string; - - #[WithCast(ContextAwareCast::class)] - public string $casted; - }; - - $data = $dataClass::from([ - 'nested' => 'Hello', - 'string' => 'world', - 'casted' => 'json:', - ]); - - expect($data)->casted - ->toEqual('json:+{"nested":{"string":"Hello"},"string":"world","casted":"json:"}'); -}); - -it('will transform native enums', function () { - $data = EnumData::from([ - 'enum' => DummyBackedEnum::FOO, - ]); - - expect($data->toArray())->toMatchArray([ - 'enum' => 'foo', - ]) - ->and($data->all())->toMatchArray([ - 'enum' => DummyBackedEnum::FOO, - ]); -}); - -it('can magically create a data object', function () { - $dataClass = new class ('', '') extends Data { - public function __construct( - public mixed $propertyA, - public mixed $propertyB, - ) { - } - - public static function fromStringWithDefault(string $a, string $b = 'World') - { - return new self($a, $b); - } - - public static function fromIntsWithDefault(int $a, int $b) - { - return new self($a, $b); - } - - public static function fromSimpleDara(SimpleData $data) - { - return new self($data->string, $data->string); - } - - public static function fromData(Data $data) - { - return new self('data', json_encode($data)); - } - }; - - expect($dataClass::from('Hello'))->toEqual(new $dataClass('Hello', 'World')) - ->and($dataClass::from('Hello', 'World'))->toEqual(new $dataClass('Hello', 'World')) - ->and($dataClass::from(42, 69))->toEqual(new $dataClass(42, 69)) - ->and($dataClass::from(SimpleData::from('Hello')))->toEqual(new $dataClass('Hello', 'Hello')) - ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); -}); - -it('can wrap data objects by method call', function () { - expect( - SimpleData::from('Hello World') - ->wrap('wrap') - ->toResponse(\request()) - ->getData(true) - )->toMatchArray(['wrap' => ['string' => 'Hello World']]); - - expect( - SimpleData::collect(['Hello', 'World'], DataCollection::class) - ->wrap('wrap') - ->toResponse(\request()) - ->getData(true) - )->toMatchArray([ - 'wrap' => [ - ['string' => 'Hello'], - ['string' => 'World'], - ], - ]); -}); - -it('can wrap data objects using a global default', function () { - config()->set('data.wrap', 'wrap'); - - expect( - SimpleData::from('Hello World') - ->toResponse(\request())->getData(true) - )->toMatchArray(['wrap' => ['string' => 'Hello World']]); - - expect( - SimpleData::from('Hello World') - ->wrap('other-wrap') - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); - - expect( - SimpleData::from('Hello World') - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['string' => 'Hello World']); - - expect( - SimpleData::collect(['Hello', 'World'], DataCollection::class) - ->toResponse(\request())->getData(true) - ) - ->toMatchArray([ - 'wrap' => [ - ['string' => 'Hello'], - ['string' => 'World'], - ], - ]); - - expect( - SimpleData::from('Hello World') - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['string' => 'Hello World']); - - expect( - (new DataCollection(SimpleData::class, ['Hello', 'World'])) - ->wrap('other-wrap') - ->toResponse(\request()) - ->getData(true) - ) - ->toMatchArray([ - 'other-wrap' => [ - ['string' => 'Hello'], - ['string' => 'World'], - ], - ]); - - expect( - (new DataCollection(SimpleData::class, ['Hello', 'World'])) - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray([ - ['string' => 'Hello'], - ['string' => 'World'], - ]); -}); - -it('can set a default wrap on a data object', function () { - expect( - SimpleDataWithWrap::from('Hello World') - ->toResponse(\request()) - ->getData(true) - ) - ->toMatchArray(['wrap' => ['string' => 'Hello World']]); - - expect( - SimpleDataWithWrap::from('Hello World') - ->wrap('other-wrap') - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); - - expect( - SimpleDataWithWrap::from('Hello World') - ->withoutWrapping() - ->toResponse(\request())->getData(true) - ) - ->toMatchArray(['string' => 'Hello World']); -}); - -it('wraps additional data', function () { - $dataClass = new class ('Hello World') extends Data { - public function __construct( - public string $string - ) { - } - - public function with(): array - { - return ['with' => 'this']; - } - }; - - $data = $dataClass->additional(['additional' => 'this']) - ->wrap('wrap') - ->toResponse(\request()) - ->getData(true); - - expect($data)->toMatchArray([ - 'wrap' => ['string' => 'Hello World'], - 'additional' => 'this', - 'with' => 'this', - ]); -}); - -it('wraps complex data structures', function () { - $data = new MultiNestedData( - new NestedData(SimpleData::from('Hello')), - [ - new NestedData(SimpleData::from('World')), - ], - ); - - expect( - $data->wrap('wrap')->toResponse(\request())->getData(true) - )->toMatchArray([ - 'wrap' => [ - 'nested' => ['simple' => ['string' => 'Hello']], - 'nestedCollection' => [ - ['simple' => ['string' => 'World']], - ], - ], - ]); -}); - -it('wraps complex data structures with a global', function () { - config()->set('data.wrap', 'wrap'); - - $data = new MultiNestedData( - new NestedData(SimpleData::from('Hello')), - [ - new NestedData(SimpleData::from('World')), - ], - ); - - expect( - $data->wrap('wrap')->toResponse(\request())->getData(true) - )->toMatchArray([ - 'wrap' => [ - 'nested' => ['simple' => ['string' => 'Hello']], - 'nestedCollection' => [ - 'wrap' => [ - ['simple' => ['string' => 'World']], - ], - ], - ], - ]); -}); - -it('only wraps responses', function () { - expect( - SimpleData::from('Hello World')->wrap('wrap') - ) - ->toArray() - ->toMatchArray(['string' => 'Hello World']); - - expect( - SimpleData::collect(['Hello', 'World'], DataCollection::class)->wrap('wrap') - ) - ->toArray() - ->toMatchArray([ - ['string' => 'Hello'], - ['string' => 'World'], - ]); -}); - -it('can use only when transforming', function (array $directive, array $expectedOnly) { - $dataClass = new class () extends Data { - public string $first; - - public string $second; - - public MultiData $nested; - - #[DataCollectionOf(MultiData::class)] - public DataCollection $collection; - }; - - $data = $dataClass::from([ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ]); - - expect($data->only(...$directive)) - ->toArray() - ->toMatchArray($expectedOnly); -})->with('only-inclusion'); - -it('can use except when transforming', function ( - array $directive, - array $expectedOnly, - array $expectedExcept -) { - $dataClass = new class () extends Data { - public string $first; - - public string $second; - - public MultiData $nested; - - #[DataCollectionOf(MultiData::class)] - public DataCollection $collection; - }; - - $data = $dataClass::from([ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ]); - - expect($data->except(...$directive)->toArray()) - ->toEqual($expectedExcept); -})->with('only-inclusion'); - -it('can use a trait', function () { +it('also works by using traits and interfaces, skipping the base data class', function () { $data = new class ('') implements DataObject { use ResponsableData; use IncludeableData; @@ -1229,9 +76,8 @@ public function with(): array use WrappableData; use TransformableData; use BaseData; - use \Spatie\LaravelData\Concerns\EmptyData; + use EmptyData; use ContextableData; - use DefaultableData; public function __construct(public string $string) { @@ -1248,112 +94,6 @@ public static function fromString(string $string): static ->and($data::from('Hi'))->toEqual(new $data('Hi')); }); -it('supports conversion from multiple date formats', function () { - $data = new class () extends Data { - public function __construct( - #[WithCast(DateTimeInterfaceCast::class, ['Y-m-d\TH:i:sP', 'Y-m-d H:i:s'])] - public ?DateTime $date = null - ) { - } - }; - - expect($data::from(['date' => '2022-05-16T14:37:56+00:00']))->toArray() - ->toMatchArray(['date' => '2022-05-16T14:37:56+00:00']) - ->and($data::from(['date' => '2022-05-16 17:00:00']))->toArray() - ->toMatchArray(['date' => '2022-05-16T17:00:00+00:00']); -}); - -it( - 'will throw a custom exception when a data constructor cannot be called due to missing component', - function () { - SimpleData::from([]); - } -)->throws(CannotCreateData::class, 'the constructor requires 1 parameters'); - -it('can inherit properties from a base class', function () { - $dataClass = new class ('') extends SimpleData { - public int $int; - }; - - $data = $dataClass::from(['string' => 'Hi', 'int' => 42]); - - expect($data) - ->string->toBe('Hi') - ->int->toBe(42); -}); - -it('can have a circular dependency', function () { - $data = CircData::from([ - 'string' => 'Hello World', - 'ular' => [ - 'string' => 'Hello World', - 'circ' => [ - 'string' => 'Hello World', - ], - ], - ]); - - expect($data)->toEqual( - new CircData('Hello World', new UlarData('Hello World', new CircData('Hello World', null))) - ); - - expect($data->toArray())->toMatchArray([ - 'string' => 'Hello World', - 'ular' => [ - 'string' => 'Hello World', - 'circ' => [ - 'string' => 'Hello World', - 'ular' => null, - ], - ], - ]); -}); - -it('can restructure payload', function () { - $class = new class () extends Data { - public function __construct( - public string|null $name = null, - public string|null $address = null, - ) { - } - - public static function prepareForPipeline(Collection $properties): Collection - { - $properties->put('address', $properties->only(['line_1', 'city', 'state', 'zipcode'])->join(',')); - - return $properties; - } - }; - - $instance = $class::from([ - 'name' => 'Freek', - 'line_1' => '123 Sesame St', - 'city' => 'New York', - 'state' => 'NJ', - 'zipcode' => '10010', - ]); - - expect($instance->toArray())->toMatchArray([ - 'name' => 'Freek', - 'address' => '123 Sesame St,New York,NJ,10010', - ]); -}); - - -it('works with livewire', function () { - $class = new class ('') extends Data { - use WireableData; - - public function __construct( - public string $name, - ) { - } - }; - - $data = $class::fromLivewire(['name' => 'Freek']); - - expect($data)->toEqual(new $class('Freek')); -}); it('can serialize and unserialize a data object', function () { $object = SimpleData::from('Hello world'); @@ -1400,166 +140,6 @@ public function __construct( expect($invaded->_dataContext)->toBeNull(); }); - -it('can set a default value for data object', function () { - $dataObject = new class ('', '') extends Data { - #[Min(10)] - public string|Optional $full_name; - - public function __construct( - public string $first_name, - public string $last_name, - ) { - $this->full_name = "{$this->first_name} {$this->last_name}"; - } - }; - - expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Versieck'); - - expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect(fn () => $dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'too short'])) - ->toThrow(ValidationException::class); -}); - -it('can have a computed value', function () { - $dataObject = new class ('', '') extends Data { - #[Computed] - public string $full_name; - - public function __construct( - public string $first_name, - public string $last_name, - ) { - $this->full_name = "{$this->first_name} {$this->last_name}"; - } - }; - - expect($dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect($dataObject::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])) - ->first_name->toBe('Ruben') - ->last_name->toBe('Van Assche') - ->full_name->toBe('Ruben Van Assche'); - - expect(fn () => $dataObject::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche', 'full_name' => 'Ruben Versieck'])) - ->toThrow(CannotSetComputedValue::class); -}); - -it('can have a nullable computed value', function () { - $dataObject = new class ('', '') extends Data { - #[Computed] - public ?string $upper_name; - - public function __construct( - public ?string $name, - ) { - $this->upper_name = $name ? strtoupper($name) : null; - } - }; - - expect($dataObject::from(['name' => 'Ruben'])) - ->name->toBe('Ruben') - ->upper_name->toBe('RUBEN'); - - expect($dataObject::from(['name' => null])) - ->name->toBeNull() - ->upper_name->toBeNull(); - - expect($dataObject::validateAndCreate(['name' => 'Ruben'])) - ->name->toBe('Ruben') - ->upper_name->toBe('RUBEN'); - - expect($dataObject::validateAndCreate(['name' => null])) - ->name->toBeNull() - ->upper_name->toBeNull(); - - expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => 'RUBEN'])) - ->toThrow(CannotSetComputedValue::class); - - expect(fn () => $dataObject::from(['name' => 'Ruben', 'upper_name' => null])) - ->name->toBeNull() - ->upper_name->toBeNull(); // Case conflicts with DefaultsPipe, ignoring it for now -}); - -it('can have a hidden value', function () { - $dataObject = new class ('', '') extends Data { - public function __construct( - public string $show, - #[Hidden] - public string $hidden, - ) { - } - }; - - expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])) - ->show->toBe('Yes') - ->hidden->toBe('No'); - - expect($dataObject::validateAndCreate(['show' => 'Yes', 'hidden' => 'No'])) - ->show->toBe('Yes') - ->hidden->toBe('No'); - - expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])->toArray())->toBe(['show' => 'Yes']); -}); - -it('throws a readable exception message when the constructor fails', function ( - array $data, - string $message, -) { - try { - MultiData::from($data); - } catch (CannotCreateData $e) { - expect($e->getMessage())->toBe($message); - - return; - } - - throw new Exception('We should not reach this point'); -})->with(fn () => [ - 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', - ]); -}); - it('can use data as an DTO', function () { $dto = SimpleDto::from('Hello World'); diff --git a/tests/Datasets/DataCollection.php b/tests/Datasets/DataCollection.php deleted file mode 100644 index cfda0e26..00000000 --- a/tests/Datasets/DataCollection.php +++ /dev/null @@ -1,11 +0,0 @@ - 'filter', - 'arguments' => [fn (SimpleData $data) => $data->string !== 'B'], - 'expected' => [0 => ['string' => 'A'], 2 => ['string' => 'C']], - ]; -}); diff --git a/tests/Datasets/DataTest.php b/tests/Datasets/DataTest.php deleted file mode 100644 index b77992b7..00000000 --- a/tests/Datasets/DataTest.php +++ /dev/null @@ -1,197 +0,0 @@ - [ - 'directive' => ['first'], - 'expectedOnly' => [ - 'first' => 'A', - ], - 'expectedExcept' => [ - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'multi' => [ - 'directive' => ['first', 'second'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - ], - 'expectedExcept' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'multi-2' => [ - 'directive' => ['{first,second}'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - ], - 'expectedExcept' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'all' => [ - 'directive' => ['*'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [], - ]; - - yield 'nested' => [ - 'directive' => ['nested'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested.single' => [ - 'directive' => ['nested.first'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested.multi' => [ - 'directive' => ['nested.{first, second}'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => [], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested-all' => [ - 'directive' => ['nested.*'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => [], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'collection' => [ - 'directive' => ['collection'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - ]; - - yield 'collection-single' => [ - 'directive' => ['collection.first'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E'], - ['first' => 'G'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['second' => 'F'], - ['second' => 'H'], - ], - ], - ]; - - yield 'collection-multi' => [ - 'directive' => ['collection.first', 'collection.second'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - [], - [], - ], - ], - ]; - - yield 'collection-all' => [ - 'directive' => ['collection.*'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - [], - [], - ], - ], - ]; -}); diff --git a/tests/EmptyTest.php b/tests/EmptyTest.php new file mode 100644 index 00000000..7f9e4d83 --- /dev/null +++ b/tests/EmptyTest.php @@ -0,0 +1,54 @@ +toMatchArray([ + 'property' => null, + 'lazyProperty' => null, + 'array' => [], + 'collection' => [], + 'dataCollection' => [], + 'data' => [ + 'string' => null, + ], + 'lazyData' => [ + 'string' => null, + ], + 'defaultProperty' => true, + ]); +}); + +it('can overwrite properties in an empty version of a data object', function () { + expect(SimpleData::empty())->toMatchArray([ + 'string' => null, + ]); + + expect(SimpleData::empty(['string' => 'Ruben']))->toMatchArray([ + 'string' => 'Ruben', + ]); +}); diff --git a/tests/DataPipes/FillRouteParameterPropertiesPipeTest.php b/tests/FillRouteParametersTest.php similarity index 100% rename from tests/DataPipes/FillRouteParameterPropertiesPipeTest.php rename to tests/FillRouteParametersTest.php diff --git a/tests/LivewireTest.php b/tests/LivewireTest.php new file mode 100644 index 00000000..72709d56 --- /dev/null +++ b/tests/LivewireTest.php @@ -0,0 +1,19 @@ + 'Freek']); + + expect($data)->toEqual(new $class('Freek')); +}); diff --git a/tests/Resolvers/DataFromSomethingResolverTest.php b/tests/MagicalCreationTest.php similarity index 99% rename from tests/Resolvers/DataFromSomethingResolverTest.php rename to tests/MagicalCreationTest.php index de05a816..9f8b2a9b 100644 --- a/tests/Resolvers/DataFromSomethingResolverTest.php +++ b/tests/MagicalCreationTest.php @@ -1,5 +1,6 @@ 'never'], + ['description' => 'gonna'], + ['description' => 'give'], + ['description' => 'you'], + ['description' => 'up'], + ]); + + $dataClass = new class ('hello', $data, $data, $dataCollection, $dataCollection) extends Data { + public function __construct( + #[MapOutputName('property')] + public string $string, + public SimpleDataWithMappedProperty $nested, + #[MapOutputName('nested_other')] + public SimpleDataWithMappedProperty $nested_renamed, + #[DataCollectionOf(SimpleDataWithMappedProperty::class)] + public array $nested_collection, + #[ + MapOutputName('nested_other_collection'), + DataCollectionOf(SimpleDataWithMappedProperty::class) + ] + public array $nested_renamed_collection, + ) { + } + }; + + expect($dataClass->toArray())->toMatchArray([ + 'property' => 'hello', + 'nested' => [ + 'description' => 'hello', + ], + 'nested_other' => [ + 'description' => 'hello', + ], + 'nested_collection' => [ + ['description' => 'never'], + ['description' => 'gonna'], + ['description' => 'give'], + ['description' => 'you'], + ['description' => 'up'], + ], + 'nested_other_collection' => [ + ['description' => 'never'], + ['description' => 'gonna'], + ['description' => 'give'], + ['description' => 'you'], + ['description' => 'up'], + ], + ]); +}); + +it('can map the property names for the whole class using one attribute when transforming', function () { + $data = DataWithMapper::from([ + 'cased_property' => 'We are the knights who say, ni!', + 'data_cased_property' => + ['string' => 'Bring us a, shrubbery!'], + 'data_collection_cased_property' => [ + ['string' => 'One that looks nice!'], + ['string' => 'But not too expensive!'], + ], + ]); + + expect($data->toArray())->toMatchArray([ + 'cased_property' => 'We are the knights who say, ni!', + 'data_cased_property' => + ['string' => 'Bring us a, shrubbery!'], + 'data_collection_cased_property' => [ + ['string' => 'One that looks nice!'], + ['string' => 'But not too expensive!'], + ], + ]); +}); + +it('can transform the data object without mapping', function () { + $data = new class ('Freek') extends Data { + public function __construct( + #[MapOutputName('snake_name')] + public string $camelName + ) { + } + }; + + expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) + ->toMatchArray([ + 'camelName' => 'Freek', + ]); +}); + +it('can map an input property using string when creating', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public string $mapped; @@ -21,7 +113,7 @@ expect($data->mapped)->toEqual('We are the knights who say, ni!'); }); -it('can map in nested objects using strings', function () { +it('can map an input property in nested objects using strings when creating', function () { $dataClass = new class () extends Data { #[MapInputName('nested.something')] public string $mapped; @@ -34,7 +126,7 @@ expect($data->mapped)->toEqual('We are the knights who say, ni!'); }); -it('replaces properties when a mapped alternative exists', function () { +it('replaces properties when a mapped alternative exists when creating', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public string $mapped; @@ -48,7 +140,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('skips properties it cannot find ', function () { +it('skips properties it cannot find when creating', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public string $mapped; @@ -61,7 +153,8 @@ expect($data->mapped)->toEqual('We are the knights who say, ni!'); }); -it('can use integers to map properties', function () { + +it('can use integers to map properties when creating', function () { $dataClass = new class () extends Data { #[MapInputName(1)] public string $mapped; @@ -75,7 +168,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('can use integers to map properties in nested data', function () { +it('can use integers to map properties in nested data when creating', function () { $dataClass = new class () extends Data { #[MapInputName('1.0')] public string $mapped; @@ -89,7 +182,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('can combine integers and strings to map properties', function () { +it('can combine integers and strings to map properties when creating', function () { $dataClass = new class () extends Data { #[MapInputName('lines.1')] public string $mapped; @@ -105,7 +198,7 @@ expect($data->mapped)->toEqual('Bring us a, shrubbery!'); }); -it('can use a dedicated mapper', function () { +it('can use a special mapping class which converts property names between standards', function () { $dataClass = new class () extends Data { #[MapInputName(SnakeCaseMapper::class)] public string $mappedLine; @@ -118,7 +211,7 @@ expect($data->mappedLine)->toEqual('We are the knights who say, ni!'); }); -it('can map properties into data objects', function () { +it('can use mapped properties to magically create data', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public SimpleData $mapped; @@ -135,7 +228,7 @@ ); }); -it('can map properties into data objects which map properties again', function () { +it('can use mapped properties (nested) to magically create data', function () { $dataClass = new class () extends Data { #[MapInputName('something')] public SimpleDataWithMappedProperty $mapped; @@ -154,7 +247,7 @@ ); }); -it('can map properties into data collections', function () { +it('can map properties when creating a collection of data objects', function () { $dataClass = new class () extends Data { #[MapInputName('something'), DataCollectionOf(SimpleData::class)] public array $mapped; @@ -177,7 +270,7 @@ ); }); -it('can map properties into data collections which map properties again', function () { +it('can map properties when creating a (nested) collection of data objects', function () { $dataClass = new class () extends Data { #[MapInputName('something'), DataCollectionOf(SimpleDataWithMappedProperty::class)] public array $mapped; @@ -200,11 +293,11 @@ ); }); -it('can map properties from a complete class', function () { +it('can use one attribute on the class to map properties when creating', function () { $data = DataWithMapper::from([ 'cased_property' => 'We are the knights who say, ni!', 'data_cased_property' => - ['string' => 'Bring us a, shrubbery!'], + ['string' => 'Bring us a, shrubbery!'], 'data_collection_cased_property' => [ ['string' => 'One that looks nice!'], ['string' => 'But not too expensive!'], diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index f4423b46..9cc3f93d 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; +use Spatie\LaravelData\Tests\Fakes\CircData; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\ExceptData; @@ -23,7 +24,10 @@ use Spatie\LaravelData\Tests\Fakes\NestedLazyData; use Spatie\LaravelData\Tests\Fakes\OnlyData; use Spatie\LaravelData\Tests\Fakes\PartialClassConditionalData; +use Spatie\LaravelData\Tests\Fakes\SimpleChildDataWithMappedOutputName; use Spatie\LaravelData\Tests\Fakes\SimpleData; +use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedOutputName; +use Spatie\LaravelData\Tests\Fakes\UlarData; it('can include a lazy property', function () { $data = new LazyData(Lazy::create(fn () => 'test')); @@ -1439,3 +1443,502 @@ public function __construct( ], ]); }); + +it('can use only when transforming', function (array $directive, array $expectedOnly) { + $dataClass = new class () extends Data { + public string $first; + + public string $second; + + public MultiData $nested; + + #[DataCollectionOf(MultiData::class)] + public DataCollection $collection; + }; + + $data = $dataClass::from([ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ]); + + expect($data->only(...$directive)) + ->toArray() + ->toMatchArray($expectedOnly); +})->with('only-inclusion'); + +it('can use except when transforming', function ( + array $directive, + array $expectedOnly, + array $expectedExcept +) { + $dataClass = new class () extends Data { + public string $first; + + public string $second; + + public MultiData $nested; + + #[DataCollectionOf(MultiData::class)] + public DataCollection $collection; + }; + + $data = $dataClass::from([ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ]); + + expect($data->except(...$directive)->toArray()) + ->toEqual($expectedExcept); +})->with('only-inclusion'); + +// Todo: replace +//it('will correctly reduce a tree based upon allowed includes', function ( +// ?array $lazyDataAllowedIncludes, +// ?array $dataAllowedIncludes, +// ?string $requestedAllowedIncludes, +// TreeNode $expectedIncludes +//) { +// LazyData::setAllowedIncludes($lazyDataAllowedIncludes); +// +// $data = new class ( +// 'Hello', +// LazyData::from('Hello'), +// LazyData::collect(['Hello', 'World']) +// ) extends Data { +// public static ?array $allowedIncludes; +// +// public function __construct( +// public string $property, +// public LazyData $nested, +// #[DataCollectionOf(LazyData::class)] +// public array $collection, +// ) { +// } +// +// public static function allowedRequestIncludes(): ?array +// { +// return static::$allowedIncludes; +// } +// }; +// +// $data::$allowedIncludes = $dataAllowedIncludes; +// +// $request = request(); +// +// if ($requestedAllowedIncludes !== null) { +// $request->merge([ +// 'include' => $requestedAllowedIncludes, +// ]); +// } +// +// $trees = $this->resolver->execute($data, $request); +// +// expect($trees->lazyIncluded)->toEqual($expectedIncludes); +//})->with(function () { +// yield 'disallowed property inclusion' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => [], +// 'requestedIncludes' => 'property', +// 'expectedIncludes' => new ExcludedTreeNode(), +// ]; +// +// yield 'allowed property inclusion' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['property'], +// 'requestedIncludes' => 'property', +// 'expectedIncludes' => new PartialTreeNode([ +// 'property' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data property inclusion without nesting' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data property inclusion with nesting' => [ +// 'lazyDataAllowedIncludes' => ['name'], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed data collection property inclusion without nesting' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['collection'], +// 'requestedIncludes' => 'collection.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'collection' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'allowed data collection property inclusion with nesting' => [ +// 'lazyDataAllowedIncludes' => ['name'], +// 'dataAllowedIncludes' => ['collection'], +// 'requestedIncludes' => 'collection.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'collection' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.name', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new PartialTreeNode([ +// 'name' => new ExcludedTreeNode(), +// ]), +// ]), +// ]; +// +// yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.*', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new AllTreeNode(), +// ]), +// ]; +// +// yield 'disallowed all nested data property inclusion ' => [ +// 'lazyDataAllowedIncludes' => [], +// 'dataAllowedIncludes' => ['nested'], +// 'requestedIncludes' => 'nested.*', +// 'expectedIncludes' => new PartialTreeNode([ +// 'nested' => new ExcludedTreeNode(), +// ]), +// ]; +// +// yield 'multi property inclusion' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested', 'property'], +// 'requestedIncludes' => 'nested.*,property', +// 'expectedIncludes' => new PartialTreeNode([ +// 'property' => new ExcludedTreeNode(), +// 'nested' => new AllTreeNode(), +// ]), +// ]; +// +// yield 'without property inclusion' => [ +// 'lazyDataAllowedIncludes' => null, +// 'dataAllowedIncludes' => ['nested', 'property'], +// 'requestedIncludes' => null, +// 'expectedIncludes' => new DisabledTreeNode(), +// ]; +//}); + +it('can combine request and manual includes', function () { + $dataclass = new class ( + Lazy::create(fn () => 'Rick Astley'), + Lazy::create(fn () => 'Never gonna give you up'), + Lazy::create(fn () => 1986), + ) extends MultiLazyData { + public static function allowedRequestIncludes(): ?array + { + return null; + } + }; + + $data = $dataclass->include('name')->toResponse(request()->merge([ + 'include' => 'artist', + ]))->getData(true); + + expect($data)->toMatchArray([ + 'artist' => 'Rick Astley', + 'name' => 'Never gonna give you up', + ]); +}); + +it('handles parsing includes from request', function (array $input, array $expected) { + $dataclass = new class ( + Lazy::create(fn () => 'Rick Astley'), + Lazy::create(fn () => 'Never gonna give you up'), + Lazy::create(fn () => 1986), + ) extends MultiLazyData { + public static function allowedRequestIncludes(): ?array + { + return ['*']; + } + }; + + $request = request()->merge($input); + + $data = $dataclass->toResponse($request)->getData(assoc: true); + + expect($data)->toHaveKeys($expected); +})->with(function () { + yield 'input as array' => [ + 'input' => ['include' => ['artist', 'name']], + 'expected' => ['artist', 'name'], + ]; + + yield 'input as comma separated' => [ + 'input' => ['include' => 'artist,name'], + 'expected' => ['artist', 'name'], + ]; +}); + +it('handles parsing except from request with mapped output name', function () { + $dataclass = SimpleDataWithMappedOutputName::from([ + 'id' => 1, + 'amount' => 1000, + 'any_string' => 'test', + 'child' => SimpleChildDataWithMappedOutputName::from([ + 'id' => 2, + 'amount' => 2000, + ]), + ]); + + $request = request()->merge(['except' => ['paid_amount', 'any_string', 'child.child_amount']]); + + $data = $dataclass->toResponse($request)->getData(assoc: true); + + expect($data)->toMatchArray([ + 'id' => 1, + 'child' => [ + 'id' => 2, + ], + ]); +}); + +it('handles circular dependencies', function () { + $dataClass = new CircData( + 'test', + new UlarData( + 'test', + new CircData('test', null) + ) + ); + + $data = $dataClass->toResponse(request())->getData(assoc: true); + + expect($data)->toBe([ + 'string' => 'test', + 'ular' => [ + 'string' => 'test', + 'circ' => [ + 'string' => 'test', + 'ular' => null, + ], + ], + ]); + + // Not really a test with expectation, we just want to check we don't end up in an infinite loop +}); + +dataset('only-inclusion', function () { + yield 'single' => [ + 'directive' => ['first'], + 'expectedOnly' => [ + 'first' => 'A', + ], + 'expectedExcept' => [ + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'multi' => [ + 'directive' => ['first', 'second'], + 'expectedOnly' => [ + 'first' => 'A', + 'second' => 'B', + ], + 'expectedExcept' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'multi-2' => [ + 'directive' => ['{first,second}'], + 'expectedOnly' => [ + 'first' => 'A', + 'second' => 'B', + ], + 'expectedExcept' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'all' => [ + 'directive' => ['*'], + 'expectedOnly' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [], + ]; + + yield 'nested' => [ + 'directive' => ['nested'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'nested.single' => [ + 'directive' => ['nested.first'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['second' => 'D'], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'nested.multi' => [ + 'directive' => ['nested.{first, second}'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => [], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'nested-all' => [ + 'directive' => ['nested.*'], + 'expectedOnly' => [ + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => [], + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + ]; + + yield 'collection' => [ + 'directive' => ['collection'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + ], + ]; + + yield 'collection-single' => [ + 'directive' => ['collection.first'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E'], + ['first' => 'G'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + ['second' => 'F'], + ['second' => 'H'], + ], + ], + ]; + + yield 'collection-multi' => [ + 'directive' => ['collection.first', 'collection.second'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + [], + [], + ], + ], + ]; + + yield 'collection-all' => [ + 'directive' => ['collection.*'], + 'expectedOnly' => [ + 'collection' => [ + ['first' => 'E', 'second' => 'F'], + ['first' => 'G', 'second' => 'H'], + ], + ], + 'expectedExcept' => [ + 'first' => 'A', + 'second' => 'B', + 'nested' => ['first' => 'C', 'second' => 'D'], + 'collection' => [ + [], + [], + ], + ], + ]; +}); diff --git a/tests/PipelineTest.php b/tests/PipelineTest.php new file mode 100644 index 00000000..79ba038a --- /dev/null +++ b/tests/PipelineTest.php @@ -0,0 +1,60 @@ +through(DefaultValuesDataPipe::class) + ->through(CastPropertiesDataPipe::class) + ->firstThrough(AuthorizedDataPipe::class); + + $reflectionProperty = tap( + new ReflectionProperty(DataPipeline::class, 'pipes'), + static fn (ReflectionProperty $r) => $r->setAccessible(true), + ); + + $pipes = $reflectionProperty->getValue($pipeline); + + expect($pipes) + ->toHaveCount(3) + ->toMatchArray([ + AuthorizedDataPipe::class, + DefaultValuesDataPipe::class, + CastPropertiesDataPipe::class, + ]); +}); + +it('can restructure payload before entering the pipeline', function () { + $class = new class () extends Data { + public function __construct( + public string|null $name = null, + public string|null $address = null, + ) { + } + + public static function prepareForPipeline(Collection $properties): Collection + { + $properties->put('address', $properties->only(['line_1', 'city', 'state', 'zipcode'])->join(',')); + + return $properties; + } + }; + + $instance = $class::from([ + 'name' => 'Freek', + 'line_1' => '123 Sesame St', + 'city' => 'New York', + 'state' => 'NJ', + 'zipcode' => '10010', + ]); + + expect($instance->toArray())->toMatchArray([ + 'name' => 'Freek', + 'address' => '123 Sesame St,New York,NJ,10010', + ]); +}); diff --git a/tests/RequestDataTest.php b/tests/RequestTest.php similarity index 82% rename from tests/RequestDataTest.php rename to tests/RequestTest.php index b58dd1ac..c9f66aee 100644 --- a/tests/RequestDataTest.php +++ b/tests/RequestTest.php @@ -2,11 +2,13 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\AuthenticationException; +use Illuminate\Foundation\Http\FormRequest; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; use Illuminate\Testing\TestResponse; use Illuminate\Validation\ValidationException; +use Spatie\LaravelData\WithData; use function Pest\Laravel\handleExceptions; use function Pest\Laravel\postJson; @@ -138,31 +140,3 @@ public static function fromRequest(Request $request) ->assertJson(['name' => 'Rick Astley']); } ); - -it('can wrap data', function () { - Route::post('/example-route', function () { - return SimpleData::from(request()->input('string'))->wrap('data'); - }); - - performRequest('Hello World') - ->assertCreated() - ->assertJson(['data' => ['string' => 'Hello World']]); -}); - -it('can wrap data collections', function () { - Route::post('/example-route', function () { - return SimpleData::collect([ - request()->input('string'), - strtoupper(request()->input('string')), - ], DataCollection::class)->wrap('data'); - }); - - performRequest('Hello World') - ->assertCreated() - ->assertJson([ - 'data' => [ - ['string' => 'Hello World'], - ['string' => 'HELLO WORLD'], - ], - ]); -}); diff --git a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php b/tests/Resolvers/PartialsTreeFromRequestResolverTest.php deleted file mode 100644 index ac0ef765..00000000 --- a/tests/Resolvers/PartialsTreeFromRequestResolverTest.php +++ /dev/null @@ -1,262 +0,0 @@ -merge([ -// 'include' => $requestedAllowedIncludes, -// ]); -// } -// -// $trees = $this->resolver->execute($data, $request); -// -// expect($trees->lazyIncluded)->toEqual($expectedIncludes); -//})->with(function () { -// yield 'disallowed property inclusion' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => [], -// 'requestedIncludes' => 'property', -// 'expectedIncludes' => new ExcludedTreeNode(), -// ]; -// -// yield 'allowed property inclusion' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['property'], -// 'requestedIncludes' => 'property', -// 'expectedIncludes' => new PartialTreeNode([ -// 'property' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data property inclusion without nesting' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data property inclusion with nesting' => [ -// 'lazyDataAllowedIncludes' => ['name'], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed data collection property inclusion without nesting' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['collection'], -// 'requestedIncludes' => 'collection.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'collection' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data collection property inclusion with nesting' => [ -// 'lazyDataAllowedIncludes' => ['name'], -// 'dataAllowedIncludes' => ['collection'], -// 'requestedIncludes' => 'collection.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'collection' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.*', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new AllTreeNode(), -// ]), -// ]; -// -// yield 'disallowed all nested data property inclusion ' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.*', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'multi property inclusion' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested', 'property'], -// 'requestedIncludes' => 'nested.*,property', -// 'expectedIncludes' => new PartialTreeNode([ -// 'property' => new ExcludedTreeNode(), -// 'nested' => new AllTreeNode(), -// ]), -// ]; -// -// yield 'without property inclusion' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested', 'property'], -// 'requestedIncludes' => null, -// 'expectedIncludes' => new DisabledTreeNode(), -// ]; -//}); - -it('can combine request and manual includes', function () { - $dataclass = new class ( - Lazy::create(fn () => 'Rick Astley'), - Lazy::create(fn () => 'Never gonna give you up'), - Lazy::create(fn () => 1986), - ) extends MultiLazyData { - public static function allowedRequestIncludes(): ?array - { - return null; - } - }; - - $data = $dataclass->include('name')->toResponse(request()->merge([ - 'include' => 'artist', - ]))->getData(true); - - expect($data)->toMatchArray([ - 'artist' => 'Rick Astley', - 'name' => 'Never gonna give you up', - ]); -}); - -it('handles parsing includes from request', function (array $input, array $expected) { - $dataclass = new class ( - Lazy::create(fn () => 'Rick Astley'), - Lazy::create(fn () => 'Never gonna give you up'), - Lazy::create(fn () => 1986), - ) extends MultiLazyData { - public static function allowedRequestIncludes(): ?array - { - return ['*']; - } - }; - - $request = request()->merge($input); - - $data = $dataclass->toResponse($request)->getData(assoc: true); - - expect($data)->toHaveKeys($expected); -})->with(function () { - yield 'input as array' => [ - 'input' => ['include' => ['artist', 'name']], - 'expected' => ['artist', 'name'], - ]; - - yield 'input as comma separated' => [ - 'input' => ['include' => 'artist,name'], - 'expected' => ['artist', 'name'], - ]; -}); - -it('handles parsing except from request with mapped output name', function () { - $dataclass = SimpleDataWithMappedOutputName::from([ - 'id' => 1, - 'amount' => 1000, - 'any_string' => 'test', - 'child' => SimpleChildDataWithMappedOutputName::from([ - 'id' => 2, - 'amount' => 2000, - ]), - ]); - - $request = request()->merge(['except' => ['paid_amount', 'any_string', 'child.child_amount']]); - - $data = $dataclass->toResponse($request)->getData(assoc: true); - - expect($data)->toMatchArray([ - 'id' => 1, - 'child' => [ - 'id' => 2, - ], - ]); -}); - -it('handles circular dependencies', function () { - $dataClass = new CircData( - 'test', - new UlarData( - 'test', - new CircData('test', null) - ) - ); - - $data = $dataClass->toResponse(request())->getData(assoc: true); - - expect($data)->toBe([ - 'string' => 'test', - 'ular' => [ - 'string' => 'test', - 'circ' => [ - 'string' => 'test', - 'ular' => null, - ], - ], - ]); - - // Not really a test with expectation, we just want to check we don't end up in an infinite loop -}); diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index cba40788..1ff83d1a 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -29,23 +29,73 @@ function findVisibleFields( public string $hidden = 'hidden'; }; - expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ 'visible' => null, ]); }); -it('will hide optional fields which are unitialized', function () { +it('will hide fields which are uninitialized', function () { $dataClass = new class () extends Data { public string $visible = 'visible'; public Optional|string $optional; }; - expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ 'visible' => null, ]); }); +it('can execute excepts', function ( + TransformationContextFactory $factory, + array $expectedVisibleFields, + array $expectedTransformed +) { + $dataClass = new class() extends Data { + /** + * @param array $collection + */ + public function __construct( + public string $string = 'string', + public SimpleData $simple = new SimpleData('simple'), + public NestedData $nested = new NestedData(new SimpleData('simple')), + public array $collection = [ + new SimpleData('simple'), + new SimpleData('simple'), + ], + ) { + } + }; + + expect(findVisibleFields($dataClass, $factory))->toEqual($expectedVisibleFields); + + expect($dataClass->transform($factory))->toEqual($expectedTransformed); +})->with(function () { + yield 'single field' => [ + 'factory' => TransformationContextFactory::create() + ->except('simple'), + 'fields' => [ + 'string' => null, + 'nested' => new TransformationContext(), + 'collection' => new TransformationContext(), + ], + 'transformed' => [ + 'string' => 'string', + 'nested' => [ + 'simple' => ['string' => 'simple'], + ], + 'collection' => [ + [ + 'simple' => 'simple', + ], + [ + 'simple' => 'simple', + ], + ], + ], + ]; +}); + // TODO write tests it('can perform an excepts', function () { diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index e915ed77..0d22cab0 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -13,7 +13,6 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\ContextableData; use Spatie\LaravelData\Contracts\DataObject; -use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\ResponsableData; @@ -594,7 +593,6 @@ function (object $class, array $expected) { AppendableData::class, ContextableData::class, BaseData::class, - DefaultableData::class, IncludeableData::class, ResponsableData::class, TransformableData::class, diff --git a/tests/TransformationTest.php b/tests/TransformationTest.php new file mode 100644 index 00000000..69b8cbc0 --- /dev/null +++ b/tests/TransformationTest.php @@ -0,0 +1,371 @@ +toArray())->toMatchArray([ + 'string' => 'Ruben', + ]); +}); + +it('can transform a collection of data objects', function () { + $collection = SimpleData::collect(collect([ + 'Ruben', + 'Freek', + 'Brent', + ]), DataCollection::class); + + expect($collection->toArray()) + ->toMatchArray([ + ['string' => 'Ruben'], + ['string' => 'Freek'], + ['string' => 'Brent'], + ]); +}); + +it('will use global transformers to convert specific types', function () { + $date = new DateTime('16 may 1994'); + + $data = new class ($date) extends Data { + public function __construct(public DateTime $date) + { + } + }; + + expect($data->toArray())->toMatchArray(['date' => '1994-05-16T00:00:00+00:00']); +}); + +it('can use a manually specified transformer', function () { + $date = new DateTime('16 may 1994'); + + $data = new class ($date) extends Data { + public function __construct( + #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] + public $date + ) { + } + }; + + expect($data->toArray())->toMatchArray(['date' => '16-05-1994']); +}); + +test('a transformer will never handle a null value', function () { + $data = new class (null) extends Data { + public function __construct( + #[WithTransformer(DateTimeInterfaceTransformer::class, 'd-m-Y')] + public $date + ) { + } + }; + + expect($data->toArray())->toMatchArray(['date' => null]); +}); + +it('can get the data object without transforming', function () { + $data = new class ( + $dataObject = new SimpleData('Test'), + $dataCollection = new DataCollection(SimpleData::class, ['A', 'B']), + Lazy::create(fn () => new SimpleData('Lazy')), + 'Test', + $transformable = new DateTime('16 may 1994') + ) extends Data { + public function __construct( + public SimpleData $data, + #[DataCollectionOf(SimpleData::class)] + public DataCollection $dataCollection, + public Lazy|Data $lazy, + public string $string, + public DateTime $transformable + ) { + } + }; + + expect($data->all())->toMatchArray([ + 'data' => $dataObject, + 'dataCollection' => $dataCollection, + 'string' => 'Test', + 'transformable' => $transformable, + ]); + + expect($data->include('lazy')->all())->toMatchArray([ + 'data' => $dataObject, + 'dataCollection' => $dataCollection, + 'lazy' => (new SimpleData('Lazy')), + 'string' => 'Test', + 'transformable' => $transformable, + ]); +}); + +it('can transform to JSON', function () { + expect('{"string":"Hello"}') + ->toEqual(SimpleData::from('Hello')->toJson()) + ->toEqual(json_encode(SimpleData::from('Hello'))); +}); + +it('can use a custom transformer for a data object and/or data collectable', function () { + $nestedData = new class (42, 'Hello World') extends Data { + public function __construct( + public int $integer, + public string $string, + ) { + } + }; + + $nestedDataCollection = $nestedData::collect([ + ['integer' => 314, 'string' => 'pi'], + ['integer' => '69', 'string' => 'Laravel after hours'], + ]); + + $dataWithDefaultTransformers = new class ($nestedData, $nestedDataCollection) extends Data { + public function __construct( + public Data $nestedData, + #[DataCollectionOf(SimpleData::class)] + public array $nestedDataCollection, + ) { + } + }; + + $dataWithSpecificTransformers = new class ($nestedData, $nestedDataCollection) extends Data { + public function __construct( + #[WithTransformer(ConfidentialDataTransformer::class)] + public Data $nestedData, + #[ + WithTransformer(ConfidentialDataCollectionTransformer::class), + DataCollectionOf(SimpleData::class) + ] + public array $nestedDataCollection, + ) { + } + }; + + expect($dataWithDefaultTransformers->toArray()) + ->toMatchArray([ + 'nestedData' => ['integer' => 42, 'string' => 'Hello World'], + 'nestedDataCollection' => [ + ['integer' => 314, 'string' => 'pi'], + ['integer' => '69', 'string' => 'Laravel after hours'], + ], + ]); + + expect($dataWithSpecificTransformers->toArray()) + ->toMatchArray([ + 'nestedData' => ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], + 'nestedDataCollection' => [ + ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], + ['integer' => 'CONFIDENTIAL', 'string' => 'CONFIDENTIAL'], + ], + ]); +}); + +it('can transform built it types with custom transformers', function () { + $data = new class ('Hello World', 'Hello World') extends Data { + public function __construct( + public string $without_transformer, + #[WithTransformer(StringToUpperTransformer::class)] + public string $with_transformer + ) { + } + }; + + expect($data->toArray())->toMatchArray([ + 'without_transformer' => 'Hello World', + 'with_transformer' => 'HELLO WORLD', + ]); +}); + +it('will not transform optional values', function () { + $dataClass = new class ('', Optional::create(), Optional::create()) extends Data { + public function __construct( + public string $string, + public string|Optional $undefinable_string, + #[WithTransformer(StringToUpperTransformer::class)] + public string|Optional $undefinable_string_with_transformer, + ) { + } + }; + + $partialData = $dataClass::from([ + 'string' => 'Hello World', + ]); + + $fullData = $dataClass::from([ + 'string' => 'Hello World', + 'undefinable_string' => 'Hello World', + 'undefinable_string_with_transformer' => 'Hello World', + ]); + + expect($partialData->toArray())->toMatchArray([ + 'string' => 'Hello World', + ]); + + expect($fullData->toArray())->toMatchArray([ + 'string' => 'Hello World', + 'undefinable_string' => 'Hello World', + 'undefinable_string_with_transformer' => 'HELLO WORLD', + ]); +}); + +it('will transform native enums', function () { + $data = EnumData::from([ + 'enum' => DummyBackedEnum::FOO, + ]); + + expect($data->toArray())->toMatchArray([ + 'enum' => 'foo', + ]) + ->and($data->all())->toMatchArray([ + 'enum' => DummyBackedEnum::FOO, + ]); +}); + +it('can have a circular dependency which will not go into an infinite loop', function () { + $data = CircData::from([ + 'string' => 'Hello World', + 'ular' => [ + 'string' => 'Hello World', + 'circ' => [ + 'string' => 'Hello World', + ], + ], + ]); + + expect($data)->toEqual( + new CircData('Hello World', new UlarData('Hello World', new CircData('Hello World', null))) + ); + + expect($data->toArray())->toMatchArray([ + 'string' => 'Hello World', + 'ular' => [ + 'string' => 'Hello World', + 'circ' => [ + 'string' => 'Hello World', + 'ular' => null, + ], + ], + ]); +}); + +it('can have a hidden value', function () { + $dataObject = new class ('', '') extends Data { + public function __construct( + public string $show, + #[Hidden] + public string $hidden, + ) { + } + }; + + expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])) + ->show->toBe('Yes') + ->hidden->toBe('No'); + + expect($dataObject::validateAndCreate(['show' => 'Yes', 'hidden' => 'No'])) + ->show->toBe('Yes') + ->hidden->toBe('No'); + + expect($dataObject::from(['show' => 'Yes', 'hidden' => 'No'])->toArray())->toBe(['show' => 'Yes']); +}); + +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', + ]); +}); + +it('can transform a paginated data collection', function () { + $items = Collection::times(100, fn (int $index) => "Item {$index}"); + + $paginator = new LengthAwarePaginator( + $items->forPage(1, 15), + 100, + 15 + ); + + $collection = new PaginatedDataCollection(SimpleData::class, $paginator); + + expect($collection)->toBeInstanceOf(PaginatedDataCollection::class); + assertMatchesJsonSnapshot($collection->toJson()); +}); + +it('can transform a paginated cursor data collection', function () { + $items = Collection::times(100, fn (int $index) => "Item {$index}"); + + $paginator = new CursorPaginator( + $items, + 15, + ); + + $collection = new CursorPaginatedDataCollection(SimpleData::class, $paginator); + + if (version_compare(app()->version(), '9.0.0', '<=')) { + $this->markTestIncomplete('Laravel 8 uses a different format'); + } + + expect($collection)->toBeInstanceOf(CursorPaginatedDataCollection::class); + assertMatchesJsonSnapshot($collection->toJson()); +}); + +it('can transform a data collection', function () { + $collection = new DataCollection(SimpleData::class, ['A', 'B']); + + $filtered = $collection->through(fn (SimpleData $data) => new SimpleData("{$data->string}x"))->toArray(); + + expect($filtered)->toMatchArray([ + ['string' => 'Ax'], + ['string' => 'Bx'], + ]); +}); + +it('can transform a data collection into JSON', function () { + $collection = (new DataCollection(SimpleData::class, ['A', 'B', 'C'])); + + expect('[{"string":"A"},{"string":"B"},{"string":"C"}]') + ->toEqual($collection->toJson()) + ->toEqual(json_encode($collection)); +}); diff --git a/tests/WithDataTest.php b/tests/WithDataTest.php new file mode 100644 index 00000000..613d5d04 --- /dev/null +++ b/tests/WithDataTest.php @@ -0,0 +1,61 @@ +fill([ + 'string' => 'Hello World', + ]); + + $data = $model->getData(); + + expect($data)->toEqual(SimpleData::from('Hello World')); +}); + +it('can define the WithData trait data class by method', function () { + $arrayable = new class () implements Arrayable { + use WithData; + + public function toArray() + { + return [ + 'string' => 'Hello World', + ]; + } + + protected function dataClass(): string + { + return SimpleData::class; + } + }; + + $data = $arrayable->getData(); + + expect($data)->toEqual(SimpleData::from('Hello World')); +}); + +it('can add the WithData trait to a request', function () { + $formRequest = new class () extends FormRequest { + use WithData; + + public string $dataClass = SimpleData::class; + }; + + $formRequest->replace([ + 'string' => 'Hello World', + ]); + + $data = $formRequest->getData(); + + expect($data)->toEqual(SimpleData::from('Hello World')); +}); diff --git a/tests/WrapTest.php b/tests/WrapTest.php new file mode 100644 index 00000000..50c39527 --- /dev/null +++ b/tests/WrapTest.php @@ -0,0 +1,233 @@ +wrap('wrap') + ->toResponse(\request()) + ->getData(true) + )->toMatchArray(['wrap' => ['string' => 'Hello World']]); + + expect( + SimpleData::collect(['Hello', 'World'], DataCollection::class) + ->wrap('wrap') + ->toResponse(\request()) + ->getData(true) + )->toMatchArray([ + 'wrap' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + ]); +}); + + + +it('can wrap data objects using a global default', function () { + config()->set('data.wrap', 'wrap'); + + expect( + SimpleData::from('Hello World') + ->toResponse(\request())->getData(true) + )->toMatchArray(['wrap' => ['string' => 'Hello World']]); + + expect( + SimpleData::from('Hello World') + ->wrap('other-wrap') + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); + + expect( + SimpleData::from('Hello World') + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['string' => 'Hello World']); + + expect( + SimpleData::collect(['Hello', 'World'], DataCollection::class) + ->toResponse(\request())->getData(true) + ) + ->toMatchArray([ + 'wrap' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + ]); + + expect( + SimpleData::from('Hello World') + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['string' => 'Hello World']); + + expect( + (new DataCollection(SimpleData::class, ['Hello', 'World'])) + ->wrap('other-wrap') + ->toResponse(\request()) + ->getData(true) + ) + ->toMatchArray([ + 'other-wrap' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + ]); + + expect( + (new DataCollection(SimpleData::class, ['Hello', 'World'])) + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray([ + ['string' => 'Hello'], + ['string' => 'World'], + ]); +}); + +it('can set a default wrap on a data object', function () { + expect( + SimpleDataWithWrap::from('Hello World') + ->toResponse(\request()) + ->getData(true) + ) + ->toMatchArray(['wrap' => ['string' => 'Hello World']]); + + expect( + SimpleDataWithWrap::from('Hello World') + ->wrap('other-wrap') + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['other-wrap' => ['string' => 'Hello World']]); + + expect( + SimpleDataWithWrap::from('Hello World') + ->withoutWrapping() + ->toResponse(\request())->getData(true) + ) + ->toMatchArray(['string' => 'Hello World']); +}); + +it('wraps additional data', function () { + $dataClass = new class ('Hello World') extends Data { + public function __construct( + public string $string + ) { + } + + public function with(): array + { + return ['with' => 'this']; + } + }; + + $data = $dataClass->additional(['additional' => 'this']) + ->wrap('wrap') + ->toResponse(\request()) + ->getData(true); + + expect($data)->toMatchArray([ + 'wrap' => ['string' => 'Hello World'], + 'additional' => 'this', + 'with' => 'this', + ]); +}); + +it('wraps complex data structures', function () { + $data = new MultiNestedData( + new NestedData(SimpleData::from('Hello')), + [ + new NestedData(SimpleData::from('World')), + ], + ); + + expect( + $data->wrap('wrap')->toResponse(\request())->getData(true) + )->toMatchArray([ + 'wrap' => [ + 'nested' => ['simple' => ['string' => 'Hello']], + 'nestedCollection' => [ + ['simple' => ['string' => 'World']], + ], + ], + ]); +}); + +it('wraps complex data structures with a global', function () { + config()->set('data.wrap', 'wrap'); + + $data = new MultiNestedData( + new NestedData(SimpleData::from('Hello')), + [ + new NestedData(SimpleData::from('World')), + ], + ); + + expect( + $data->wrap('wrap')->toResponse(\request())->getData(true) + )->toMatchArray([ + 'wrap' => [ + 'nested' => ['simple' => ['string' => 'Hello']], + 'nestedCollection' => [ + 'wrap' => [ + ['simple' => ['string' => 'World']], + ], + ], + ], + ]); +}); + +it('only wraps responses, default transformations will not wrap', function () { + expect( + SimpleData::from('Hello World')->wrap('wrap') + ) + ->toArray() + ->toMatchArray(['string' => 'Hello World']); + + expect( + SimpleData::collect(['Hello', 'World'], DataCollection::class)->wrap('wrap') + ) + ->toArray() + ->toMatchArray([ + ['string' => 'Hello'], + ['string' => 'World'], + ]); +}); + +it('will wrap responses which are data', function () { + Route::post('/example-route', function () { + return SimpleData::from(request()->input('string'))->wrap('data'); + }); + + performRequest('Hello World') + ->assertCreated() + ->assertJson(['data' => ['string' => 'Hello World']]); +}); + +it('will wrap responses which are data collections', function () { + Route::post('/example-route', function () { + return SimpleData::collect([ + request()->input('string'), + strtoupper(request()->input('string')), + ], DataCollection::class)->wrap('data'); + }); + + performRequest('Hello World') + ->assertCreated() + ->assertJson([ + 'data' => [ + ['string' => 'Hello World'], + ['string' => 'HELLO WORLD'], + ], + ]); +}); diff --git a/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json new file mode 100644 index 00000000..79c64f04 --- /dev/null +++ b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_cursor_data_collection__1.json @@ -0,0 +1,58 @@ +{ + "data": [ + { + "string": "Item 1" + }, + { + "string": "Item 2" + }, + { + "string": "Item 3" + }, + { + "string": "Item 4" + }, + { + "string": "Item 5" + }, + { + "string": "Item 6" + }, + { + "string": "Item 7" + }, + { + "string": "Item 8" + }, + { + "string": "Item 9" + }, + { + "string": "Item 10" + }, + { + "string": "Item 11" + }, + { + "string": "Item 12" + }, + { + "string": "Item 13" + }, + { + "string": "Item 14" + }, + { + "string": "Item 15" + } + ], + "links": [], + "meta": { + "path": "\/", + "per_page": 15, + "next_cursor": "eyJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "next_page_url": "\/?cursor=eyJfcG9pbnRzVG9OZXh0SXRlbXMiOnRydWV9", + "prev_cursor": null, + "prev_page_url": null + } +} diff --git a/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json new file mode 100644 index 00000000..c4ce0e42 --- /dev/null +++ b/tests/__snapshots__/TransformationTest__it_can_transform_a_paginated_data_collection__1.json @@ -0,0 +1,109 @@ +{ + "data": [ + { + "string": "Item 1" + }, + { + "string": "Item 2" + }, + { + "string": "Item 3" + }, + { + "string": "Item 4" + }, + { + "string": "Item 5" + }, + { + "string": "Item 6" + }, + { + "string": "Item 7" + }, + { + "string": "Item 8" + }, + { + "string": "Item 9" + }, + { + "string": "Item 10" + }, + { + "string": "Item 11" + }, + { + "string": "Item 12" + }, + { + "string": "Item 13" + }, + { + "string": "Item 14" + }, + { + "string": "Item 15" + } + ], + "links": [ + { + "url": null, + "label": "« Previous", + "active": false + }, + { + "url": "\/?page=1", + "label": "1", + "active": true + }, + { + "url": "\/?page=2", + "label": "2", + "active": false + }, + { + "url": "\/?page=3", + "label": "3", + "active": false + }, + { + "url": "\/?page=4", + "label": "4", + "active": false + }, + { + "url": "\/?page=5", + "label": "5", + "active": false + }, + { + "url": "\/?page=6", + "label": "6", + "active": false + }, + { + "url": "\/?page=7", + "label": "7", + "active": false + }, + { + "url": "\/?page=2", + "label": "Next »", + "active": false + } + ], + "meta": { + "current_page": 1, + "first_page_url": "\/?page=1", + "from": 1, + "last_page": 7, + "last_page_url": "\/?page=7", + "next_page_url": "\/?page=2", + "path": "\/", + "per_page": 15, + "prev_page_url": null, + "to": 15, + "total": 100 + } +} From 3d006232e3b15beeeb3006c658c02436d039eecd Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Mon, 15 Jan 2024 13:25:36 +0000 Subject: [PATCH 072/124] Fix styling --- src/Data.php | 1 - src/Resource.php | 2 - src/Support/DataClass.php | 1 - tests/AppendTest.php | 1 - tests/DataCollectionTest.php | 9 ---- tests/DataTest.php | 47 ------------------- tests/RequestTest.php | 3 -- .../VisibleDataFieldsResolverTest.php | 2 +- tests/TransformationTest.php | 1 + 9 files changed, 2 insertions(+), 65 deletions(-) diff --git a/src/Data.php b/src/Data.php index 3ab8fb1b..a6c4b43e 100644 --- a/src/Data.php +++ b/src/Data.php @@ -5,7 +5,6 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; -use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; diff --git a/src/Resource.php b/src/Resource.php index 47cfab6d..5358c12d 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -5,7 +5,6 @@ use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; -use Spatie\LaravelData\Concerns\DefaultableData; use Spatie\LaravelData\Concerns\EmptyData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; @@ -13,7 +12,6 @@ use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\AppendableData as AppendableDataContract; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; -use Spatie\LaravelData\Contracts\DefaultableData as DefaultDataContract; use Spatie\LaravelData\Contracts\EmptyData as EmptyDataContract; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index f337bc7c..9b4bf41f 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -10,7 +10,6 @@ use ReflectionProperty; use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\DataObject; -use Spatie\LaravelData\Contracts\DefaultableData; use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\ResponsableData; diff --git a/tests/AppendTest.php b/tests/AppendTest.php index d7e2b277..81298674 100644 --- a/tests/AppendTest.php +++ b/tests/AppendTest.php @@ -83,4 +83,3 @@ public function with(): array 'alt_name' => 'I m Freek from additional', ]); }); - diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 71ebcfd4..a1f3db9d 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -3,21 +3,13 @@ use Illuminate\Pagination\AbstractPaginator; use Illuminate\Pagination\CursorPaginator; use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; -use Spatie\LaravelData\Concerns\WithDeprecatedCollectionMethod; -use Spatie\LaravelData\Contracts\DeprecatedData as DeprecatedDataContract; -use Spatie\LaravelData\CursorPaginatedDataCollection; -use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\Collections\CustomCollection; -use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; -use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\SimpleData; -use function Spatie\Snapshots\assertMatchesJsonSnapshot; use function Spatie\Snapshots\assertMatchesSnapshot; it('can filter a collection', function () { @@ -284,4 +276,3 @@ expect($collection[0])->toBeInstanceOf(SimpleData::class); expect($collection[1])->toBeInstanceOf(SimpleData::class); }); - diff --git a/tests/DataTest.php b/tests/DataTest.php index 31042b02..800e2927 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1,21 +1,6 @@ $collection */ diff --git a/tests/TransformationTest.php b/tests/TransformationTest.php index 69b8cbc0..0dd8c7e0 100644 --- a/tests/TransformationTest.php +++ b/tests/TransformationTest.php @@ -25,6 +25,7 @@ use Spatie\LaravelData\Tests\Fakes\UlarData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; use Spatie\LaravelData\Transformers\Transformer; + use function Spatie\Snapshots\assertMatchesJsonSnapshot; it('can transform a data object', function () { From 06fcfe6b737e3380cfad202f7320c468e567fe6d Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 16 Jan 2024 13:46:52 +0100 Subject: [PATCH 073/124] Partial tests --- composer.json | 5 +- src/Concerns/BaseData.php | 6 +- .../DataCollectableAnnotationReader.php | 4 +- src/Support/Creation/CreationContext.php | 10 +- .../Creation/CreationContextFactory.php | 17 +- src/Support/Partials/ResolvedPartial.php | 8 + .../Partials/ResolvedPartialsCollection.php | 22 + .../Transformation/TransformationContext.php | 14 + tests/Fakes/CollectionAnnotationsData.php | 33 +- .../VisibleDataFieldsResolverTest.php | 511 ++++++++++++++++-- .../DataCollectableAnnotationReaderTest.php | 17 +- tests/TransformationTest.php | 4 +- 12 files changed, 564 insertions(+), 87 deletions(-) diff --git a/composer.json b/composer.json index c5a865f6..d4130cdf 100644 --- a/composer.json +++ b/composer.json @@ -43,13 +43,12 @@ }, "autoload" : { "psr-4" : { - "Spatie\\LaravelData\\" : "src", - "Spatie\\LaravelData\\Database\\Factories\\" : "database/factories" + "Spatie\\LaravelData\\" : "src/" } }, "autoload-dev" : { "psr-4" : { - "Spatie\\LaravelData\\Tests\\" : "tests" + "Spatie\\LaravelData\\Tests\\" : "tests/" } }, "scripts" : { diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index bc0b85b9..93250213 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -95,10 +95,12 @@ public function getMorphClass(): string public function __sleep(): array { - return app(DataConfig::class)->getDataClass(static::class) + $dataClass = app(DataConfig::class)->getDataClass(static::class); + + return $dataClass ->properties ->map(fn (DataProperty $property) => $property->name) - ->push('_additional') + ->when($dataClass->appendable, fn(Collection $properties) => $properties->push('_additional')) ->toArray(); } } diff --git a/src/Support/Annotations/DataCollectableAnnotationReader.php b/src/Support/Annotations/DataCollectableAnnotationReader.php index bcc5312f..86e4a472 100644 --- a/src/Support/Annotations/DataCollectableAnnotationReader.php +++ b/src/Support/Annotations/DataCollectableAnnotationReader.php @@ -15,6 +15,9 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; +/** + * @note To myself, always use the fully qualified class names in pest tests when using anonymous classes + */ class DataCollectableAnnotationReader { /** @var array */ @@ -220,7 +223,6 @@ protected function resolveFcqn( return ltrim((string) $type, '\\'); } - protected function getContext(ReflectionProperty|ReflectionClass|ReflectionMethod $reflection): Context { $reflectionClass = $reflection instanceof ReflectionProperty || $reflection instanceof ReflectionMethod diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php index 397e9c96..255fe41a 100644 --- a/src/Support/Creation/CreationContext.php +++ b/src/Support/Creation/CreationContext.php @@ -29,11 +29,11 @@ class CreationContext */ public function __construct( public string $dataClass, - public ValidationType $validationType, - public bool $mapPropertyNames, - public bool $withoutMagicalCreation, - public ?array $ignoredMagicalMethods, - public ?GlobalCastsCollection $casts, + public readonly ValidationType $validationType, + public readonly bool $mapPropertyNames, + public readonly bool $withoutMagicalCreation, + public readonly ?array $ignoredMagicalMethods, + public readonly ?GlobalCastsCollection $casts, ) { } diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 12be1c65..411cb8e8 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -55,6 +55,19 @@ public static function createFromConfig( ); } + public static function createFromContext( + CreationContext $context + ) { + return new self( + dataClass: $context->dataClass, + validationType: $context->validationType, + mapPropertyNames: $context->mapPropertyNames, + withoutMagicalCreation: $context->withoutMagicalCreation, + ignoredMagicalMethods: $context->ignoredMagicalMethods, + casts: $context->casts, + ); + } + public function validationType(ValidationType $validationType): self { $this->validationType = $validationType; @@ -112,7 +125,7 @@ public function ignoreMagicalMethod(string ...$methods): self */ public function withCast( string $castable, - Cast|string $cast, + Cast | string $cast, ): self { $cast = is_string($cast) ? app($cast) : $cast; @@ -174,7 +187,7 @@ public function from(mixed ...$payloads): BaseData public function collect( mixed $items, ?string $into = null - ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection { + ): array | DataCollection | PaginatedDataCollection | CursorPaginatedDataCollection | Enumerable | AbstractPaginator | PaginatorContract | AbstractCursorPaginator | CursorPaginatorContract | LazyCollection | Collection { return DataContainer::get()->dataCollectableFromSomethingResolver()->execute( $this->dataClass, $this->get(), diff --git a/src/Support/Partials/ResolvedPartial.php b/src/Support/Partials/ResolvedPartial.php index bd0d0f32..f898c94d 100644 --- a/src/Support/Partials/ResolvedPartial.php +++ b/src/Support/Partials/ResolvedPartial.php @@ -108,6 +108,14 @@ public function toLaravel(): array return [implode('.', $segments)]; } + public function toArray(): array + { + return [ + 'segments' => $this->segments, + 'pointer' => $this->pointer, + ]; + } + public function next(): self { $this->pointer++; diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php index 1e8807e5..b1c10ac7 100644 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -10,6 +10,28 @@ */ class ResolvedPartialsCollection extends SplObjectStorage implements Stringable { + public static function create(ResolvedPartial ...$resolvedPartials): self + { + $collection = new self(); + + foreach ($resolvedPartials as $resolvedPartial) { + $collection->attach($resolvedPartial); + } + + return $collection; + } + + public function toArray(): array + { + $output = []; + + foreach ($this as $resolvedPartial) { + $output[] = $resolvedPartial->toArray(); + } + + return $output; + } + public function __toString(): string { $output = "- excludedPartials:".PHP_EOL; diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index f906f5b6..fa285015 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -176,6 +176,20 @@ public function mergePartialsFromDataContext( return $this; } + public function toArray(): array + { + return [ + 'transformValues' => $this->transformValues, + 'mapPropertyNames' => $this->mapPropertyNames, + 'wrapExecutionType' => $this->wrapExecutionType, + 'transformers' => $this->transformers !== null ? iterator_to_array($this->transformers) : null, + 'includedPartials' => $this->includedPartials?->toArray(), + 'excludedPartials' => $this->excludedPartials?->toArray(), + 'onlyPartials' => $this->onlyPartials?->toArray(), + 'exceptPartials' => $this->exceptPartials?->toArray(), + ]; + } + public function __clone(): void { if ($this->includedPartials !== null) { diff --git a/tests/Fakes/CollectionAnnotationsData.php b/tests/Fakes/CollectionAnnotationsData.php index 20180ad7..19330e49 100644 --- a/tests/Fakes/CollectionAnnotationsData.php +++ b/tests/Fakes/CollectionAnnotationsData.php @@ -71,22 +71,27 @@ class CollectionAnnotationsData public array $propertyT; /** - * @param \Spatie\LaravelData\Tests\Fakes\SimpleData[]|null $propertyA - * @param null|\Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyB - * @param ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyC - * @param ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] $propertyD - * @param \Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $propertyE - * @param ?\Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $propertyF - * @param SimpleData[] $propertyG + * @param \Spatie\LaravelData\Tests\Fakes\SimpleData[]|null $paramA + * @param null|\Spatie\LaravelData\Tests\Fakes\SimpleData[] $paramB + * @param ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] $paramC + * @param ?\Spatie\LaravelData\Tests\Fakes\SimpleData[] $paramD + * @param \Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $paramE + * @param ?\Spatie\LaravelData\DataCollection<\Spatie\LaravelData\Tests\Fakes\SimpleData> $paramF + * @param SimpleData[] $paramG + * @param array $paramH + * @param array $paramJ + * @param array $paramI */ public function method( - array $propertyA, - ?array $propertyB, - ?array $propertyC, - array $propertyD, - DataCollection $propertyE, - ?DataCollection $propertyF, - array $propertyG, + array $paramA, + ?array $paramB, + ?array $paramC, + array $paramD, + DataCollection $paramE, + ?DataCollection $paramF, + array $paramG, + array $paramJ, + array $paramI, ) { } diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index 373a81d3..1f5e2063 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -1,14 +1,18 @@ $collection - */ - public function __construct( - public string $string = 'string', - public SimpleData $simple = new SimpleData('simple'), - public NestedData $nested = new NestedData(new SimpleData('simple')), - public array $collection = [ - new SimpleData('simple'), - new SimpleData('simple'), - ], - ) { - } - }; + $data = VisibleFieldsData::instance(); + + $visibleFields = findVisibleFields($data, $factory); + + $visibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $visibleFields); + $expectedVisibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $expectedVisibleFields); - expect(findVisibleFields($dataClass, $factory))->toEqual($expectedVisibleFields); + expect($visibleFields)->toEqual($expectedVisibleFields); - expect($dataClass->transform($factory))->toEqual($expectedTransformed); + expect($data->transform($factory))->toEqual($expectedTransformed); })->with(function () { yield 'single field' => [ 'factory' => TransformationContextFactory::create() - ->except('simple'), + ->except('single'), 'fields' => [ 'string' => null, + 'int' => null, + 'nested' => new TransformationContext(), + 'collection' => new TransformationContext(), + ], + 'transformed' => [ + 'string' => 'hello', + 'int' => 42, + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->except('{string,int,single}'), + 'fields' => [ 'nested' => new TransformationContext(), 'collection' => new TransformationContext(), ], 'transformed' => [ - 'string' => 'string', 'nested' => [ - 'simple' => ['string' => 'simple'], + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], ], 'collection' => [ - [ - 'simple' => 'simple', - ], - [ - 'simple' => 'simple', - ], + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'all' => [ + 'factory' => TransformationContextFactory::create() + ->except('*'), + 'fields' => [], + 'transformed' => [], + ]; + + yield 'nested data object single field' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single', 'collection') // ignore non nested object fields + ->except('nested.a'), + 'fields' => [ + 'nested' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'b' => ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single', 'collection') // ignore non nested object fields + ->except('nested.{a,b}'), + 'fields' => [ + 'nested' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [], + ], + ]; + + yield 'nested data object all' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single', 'collection') // ignore non nested object fields + ->except('nested.*'), + 'fields' => [ + 'nested' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [], + ], + ]; + + yield 'nested data collectable single field' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single', 'nested') // ignore non collection fields + ->except('collection.string'), + 'fields' => [ + 'collection' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['int' => 42], + ['int' => 42], + ], + ], + ]; + + yield 'nested data collectable multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single', 'nested') // ignore non collection fields + ->except('collection.{string,int}'), + 'fields' => [ + 'collection' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + [], + [], + ], + ], + ]; + + yield 'nested data collectable all' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single', 'nested') // ignore non collection fields + ->except('collection.*'), + 'fields' => [ + 'collection' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + [], + [], + ], + ], + ]; + + yield 'combination' => [ + 'factory' => TransformationContextFactory::create() + ->except('string', 'int', 'single.string') + ->except('collection.string') + ->except('nested.a.string'), + 'fields' => [ + 'single' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'collection' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'nested' => new TransformationContext( + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a') , new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'single' => ['int' => 42], + 'collection' => [ + ['int' => 42], + ['int' => 42], + ], + 'nested' => [ + 'a' => ['int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], ], ], ]; }); -// TODO write tests - -it('can perform an excepts', function () { - // $dataClass = new class() extends Data { - // public function __construct( - // public string $visible = 'visible', - // public SimpleData $simple = new SimpleData('simple'), - // public NestedData $nestedData = new NestedData(new SimpleData('simple')), - // public array $collection = - // ) { - // } - // }; - // - // expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toMatchArray([ - // 'multi' => new TransformationContext( - // transformValues: true, - // mapPropertyNames: true, - // wrapExecutionType: true, - // new SplObjectStorage(), - // new SplObjectStorage(), - // new SplObjectStorage(), - // new SplObjectStorage(), - // ), - // ]); +it("can execute only's", function ( + TransformationContextFactory $factory, + array $expectedVisibleFields, + array $expectedTransformed +) { + $data = VisibleFieldsData::instance(); + + $visibleFields = findVisibleFields($data, $factory); + + $visibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $visibleFields); + $expectedVisibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $expectedVisibleFields); + + expect($visibleFields)->toEqual($expectedVisibleFields); + + expect($data->transform($factory))->toEqual($expectedTransformed); +})->with(function () { + yield 'single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('single'), + 'fields' => [ + 'single' => new TransformationContext(), + ], + 'transformed' => [ + 'single' => ['string' => 'hello', 'int' => 42,], + ], + ]; + + yield 'multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('{string,int,single}'), + 'fields' => [ + 'string' => null, + 'int' => null, + 'single' => new TransformationContext(), + ], + 'transformed' => [ + 'string' => 'hello', + 'int' => 42, + 'single' => ['string' => 'hello', 'int' => 42,], + ], + ]; + + yield 'all' => [ + 'factory' => TransformationContextFactory::create() + ->only('*'), + 'fields' => [ + 'string' => null, + 'int' => null, + 'single' => new TransformationContext(), + 'nested' => new TransformationContext(), + 'collection' => new TransformationContext(), + ], + 'transformed' => [ + 'string' => 'hello', + 'int' => 42, + 'single' => ['string' => 'hello', 'int' => 42,], + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested.a'), + 'fields' => [ + 'nested' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested.{a,b}'), + 'fields' => [ + 'nested' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object all' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested.*'), + 'fields' => [ + 'nested' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data collectable single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection.string'), + 'fields' => [ + 'collection' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['string' => 'hello'], + ['string' => 'hello'], + ], + ], + ]; + + yield 'nested data collectable multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection.{string,int}'), + 'fields' => [ + 'collection' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data collectable all' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection.*'), + 'fields' => [ + 'collection' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'combination' => [ + 'factory' => TransformationContextFactory::create() + ->only('string', 'single.string') + ->only('collection.string') + ->only('nested.a.string'), + 'fields' => [ + 'string' => null, + 'single' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'collection' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'nested' => new TransformationContext( + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a') , new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'string' => 'hello', + 'single' => ['string' => 'hello'], + 'collection' => [ + ['string' => 'hello'], + ['string' => 'hello'], + ], + 'nested' => [ + 'a' => ['string' => 'hello'], + ], + ], + ]; }); diff --git a/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php b/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php index cdff60e5..f201d651 100644 --- a/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php +++ b/tests/Support/Annotations/DataCollectableAnnotationReaderTest.php @@ -101,12 +101,15 @@ function (string $property, ?DataCollectableAnnotation $expected) { $annotations = app(DataCollectableAnnotationReader::class)->getForMethod(new ReflectionMethod(CollectionAnnotationsData::class, 'method')); expect($annotations)->toEqualCanonicalizing([ - new DataCollectableAnnotation(SimpleData::class, property: 'propertyA'), - new DataCollectableAnnotation(SimpleData::class, property: 'propertyB'), - new DataCollectableAnnotation(SimpleData::class, property: 'propertyC'), - new DataCollectableAnnotation(SimpleData::class, property: 'propertyD'), - new DataCollectableAnnotation(SimpleData::class, property: 'propertyE'), - new DataCollectableAnnotation(SimpleData::class, property: 'propertyF'), - new DataCollectableAnnotation(SimpleData::class, property: 'propertyG'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramA'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramB'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramC'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramD'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramE'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramF'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramG'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramH'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramJ'), + new DataCollectableAnnotation(SimpleData::class, property: 'paramI'), ]); }); diff --git a/tests/TransformationTest.php b/tests/TransformationTest.php index 0dd8c7e0..84f0aca2 100644 --- a/tests/TransformationTest.php +++ b/tests/TransformationTest.php @@ -12,10 +12,13 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; +use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Fakes\CircData; +use Spatie\LaravelData\Tests\Fakes\CollectionAnnotationsData; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -25,7 +28,6 @@ use Spatie\LaravelData\Tests\Fakes\UlarData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; use Spatie\LaravelData\Transformers\Transformer; - use function Spatie\Snapshots\assertMatchesJsonSnapshot; it('can transform a data object', function () { From 1c1bde5069d77de25132be13b24de9bbde2e5767 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 16 Jan 2024 15:06:05 +0100 Subject: [PATCH 074/124] wip --- .../DataCollectableFromSomethingResolver.php | 43 +++- src/Resolvers/DataFromSomethingResolver.php | 22 +- src/Support/DataMethod.php | 4 + src/Support/DataParameter.php | 9 +- tests/CreationTest.php | 94 -------- tests/MagicalCreationTest.php | 223 +++++++++++++++++- tests/Support/DataParameterTest.php | 29 ++- 7 files changed, 303 insertions(+), 121 deletions(-) diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 309950e7..4a75a109 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -78,8 +78,17 @@ protected function createFromCustomCreationMethod( $method = $this->dataConfig ->getDataClass($dataClass) ->methods - ->filter(function (DataMethod $method) use ($into, $items) { - if ($method->customCreationMethodType !== CustomCreationMethodType::Collection) { + ->filter(function (DataMethod $method) use ($creationContext, $into, $items) { + if ( + $method->customCreationMethodType !== CustomCreationMethodType::Collection + ) { + return false; + } + + if ( + $creationContext->ignoredMagicalMethods !== null + && in_array($method->name, $creationContext->ignoredMagicalMethods) + ) { return false; } @@ -87,24 +96,32 @@ protected function createFromCustomCreationMethod( return false; } - return $method->accepts([$items]); + return $method->accepts($items); }) ->first(); - if ($method !== null) { - return $dataClass::{$method->name}( - array_map($this->itemsToDataClosure($dataClass, $creationContext), $items) - ); + if ($method === null) { + return null; + } + + $payload = []; + + foreach ($method->parameters as $parameter) { + if ($parameter->isCreationContext) { + $payload[$parameter->name] = $creationContext; + } else { + $payload[$parameter->name] = $this->normalizeItems($items, $dataClass, $creationContext); + } } - return null; + return $dataClass::{$method->name}(...$payload); } protected function normalizeItems( mixed $items, string $dataClass, CreationContext $creationContext, - ): array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator { + ): array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator|Enumerable { if ($items instanceof PaginatedDataCollection || $items instanceof CursorPaginatedDataCollection || $items instanceof DataCollection @@ -120,7 +137,7 @@ protected function normalizeItems( } if ($items instanceof Enumerable) { - $items = $items->all(); + return $items->map($this->itemsToDataClosure($dataClass, $creationContext)); } if (is_array($items)) { @@ -134,8 +151,12 @@ protected function normalizeItems( } protected function normalizeToArray( - array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator $items, + array|Paginator|AbstractPaginator|CursorPaginator|AbstractCursorPaginator|Enumerable $items, ): array { + if ($items instanceof Enumerable) { + return $items->all(); + } + return is_array($items) ? $items : $items->items(); diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 953621e0..5317121b 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -60,25 +60,28 @@ protected function createFromCustomCreationMethod( ->getDataClass($class) ->methods; - $methodName = null; + $method = null; foreach ($customCreationMethods as $customCreationMethod) { + if ($customCreationMethod->customCreationMethodType !== CustomCreationMethodType::Object) { + continue; + } + if ( - $customCreationMethod->customCreationMethodType === CustomCreationMethodType::Object - && $creationContext->ignoredMagicalMethods !== null + $creationContext->ignoredMagicalMethods !== null && in_array($customCreationMethod->name, $creationContext->ignoredMagicalMethods) ) { continue; } if ($customCreationMethod->accepts(...$payloads)) { - $methodName = $customCreationMethod->name; + $method = $customCreationMethod; break; } } - if ($methodName === null) { + if ($method === null) { return null; } @@ -86,10 +89,19 @@ protected function createFromCustomCreationMethod( foreach ($payloads as $payload) { if ($payload instanceof Request) { + // Solely for the purpose of validation $pipeline->execute($payload, $creationContext); } } + foreach ($method->parameters as $index => $parameter) { + if ($parameter->isCreationContext) { + $payloads[$index] = $creationContext; + } + } + + $methodName = $method->name; + return $class::$methodName(...$payloads); } } diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index c788ba26..6ccca9d4 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -105,6 +105,10 @@ public function accepts(mixed ...$input): bool ? $this->parameters : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); + $parameters = $parameters->reject( + fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->isCreationContext + ); + if (count($input) > $parameters->count()) { return false; } diff --git a/src/Support/DataParameter.php b/src/Support/DataParameter.php index c7ed953d..8643763d 100644 --- a/src/Support/DataParameter.php +++ b/src/Support/DataParameter.php @@ -3,6 +3,8 @@ namespace Spatie\LaravelData\Support; use ReflectionParameter; +use Spatie\LaravelData\Support\Creation\CreationContext; +use Spatie\LaravelData\Support\Types\SingleType; use Spatie\LaravelData\Support\Types\Type; class DataParameter @@ -13,6 +15,8 @@ public function __construct( public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, public readonly Type $type, + // TODO: would be better if we refactor this to type, together with Castable, Lazy, etc + public readonly bool $isCreationContext, ) { } @@ -22,12 +26,15 @@ public static function create( ): self { $hasDefaultValue = $parameter->isDefaultValueAvailable(); + $type = Type::forReflection($parameter->getType(), $class); + return new self( $parameter->name, $parameter->isPromoted(), $hasDefaultValue, $hasDefaultValue ? $parameter->getDefaultValue() : null, - Type::forReflection($parameter->getType(), $class), + $type, + $type instanceof SingleType && $type->type->name === CreationContext::class ); } } diff --git a/tests/CreationTest.php b/tests/CreationTest.php index 3fa0cb13..7893413f 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -579,42 +579,6 @@ public function __construct( ->toEqual('json:+{"nested":{"string":"Hello"},"string":"world","casted":"json:"}'); }); -it('can magically create a data object', function () { - $dataClass = new class ('', '') extends Data { - public function __construct( - public mixed $propertyA, - public mixed $propertyB, - ) { - } - - public static function fromStringWithDefault(string $a, string $b = 'World') - { - return new self($a, $b); - } - - public static function fromIntsWithDefault(int $a, int $b) - { - return new self($a, $b); - } - - public static function fromSimpleDara(SimpleData $data) - { - return new self($data->string, $data->string); - } - - public static function fromData(Data $data) - { - return new self('data', json_encode($data)); - } - }; - - expect($dataClass::from('Hello'))->toEqual(new $dataClass('Hello', 'World')) - ->and($dataClass::from('Hello', 'World'))->toEqual(new $dataClass('Hello', 'World')) - ->and($dataClass::from(42, 69))->toEqual(new $dataClass(42, 69)) - ->and($dataClass::from(SimpleData::from('Hello')))->toEqual(new $dataClass('Hello', 'Hello')) - ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); -}); - it( 'will throw a custom exception when a data constructor cannot be called due to missing component', function () { @@ -762,32 +726,6 @@ public function __construct( ->toMatchArray($collectionA->toArray()); }); -it('will use magic methods when creating a collection of data objects', function () { - $dataClass = new class ('') extends Data { - public function __construct(public string $otherString) - { - } - - public static function fromSimpleData(SimpleData $simpleData): static - { - return new self($simpleData->string); - } - }; - - $collection = new DataCollection($dataClass::class, [ - SimpleData::from('A'), - SimpleData::from('B'), - ]); - - expect($collection[0]) - ->toBeInstanceOf($dataClass::class) - ->otherString->toEqual('A'); - - expect($collection[1]) - ->toBeInstanceOf($dataClass::class) - ->otherString->toEqual('B'); -}); - it('can return a custom data collection when collecting data', function () { $class = new class ('') extends Data implements DeprecatedDataContract { use WithDeprecatedCollectionMethod; @@ -823,38 +761,6 @@ public function __construct(public string $string) expect($collection)->toBeInstanceOf(CustomPaginatedDataCollection::class); }); -it('can magically collect data', function () { - class TestSomeCustomCollection extends Collection - { - } - - $dataClass = new class () extends Data { - public string $string; - - public static function fromString(string $string): self - { - $s = new self(); - - $s->string = $string; - - return $s; - } - - public static function collectArray(array $items): \TestSomeCustomCollection - { - return new \TestSomeCustomCollection($items); - } - }; - - expect($dataClass::collect(['a', 'b', 'c'])) - ->toBeInstanceOf(\TestSomeCustomCollection::class) - ->all()->toEqual([ - $dataClass::from('a'), - $dataClass::from('b'), - $dataClass::from('c'), - ]); -}); - it('will allow a nested data object to cast properties however it wants', function () { $model = new DummyModel(['id' => 10]); diff --git a/tests/MagicalCreationTest.php b/tests/MagicalCreationTest.php index 9f8b2a9b..b894a2c8 100644 --- a/tests/MagicalCreationTest.php +++ b/tests/MagicalCreationTest.php @@ -3,15 +3,21 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Collection; use Spatie\LaravelData\Data; +use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMultipleArgumentCreationMethod; use Spatie\LaravelData\Tests\Fakes\DummyDto; +use Spatie\LaravelData\Tests\Fakes\EnumData; +use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; use Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCasts; +use Spatie\LaravelData\Tests\Fakes\SimpleData; -it('can create data from a custom method', function () { +it('can create data using a magical method', function () { $data = new class ('') extends Data { public function __construct(public string $string) { @@ -40,7 +46,43 @@ public static function fromArray(array $payload) ->and($data::from(DummyModelWithCasts::make(['string' => 'Hello World'])))->toEqual(new $data('Hello World')); }); -it('can create data from a custom method with an interface parameter', function () { +it('can magically create a data object', function () { + $dataClass = new class ('', '') extends Data { + public function __construct( + public mixed $propertyA, + public mixed $propertyB, + ) { + } + + public static function fromStringWithDefault(string $a, string $b = 'World') + { + return new self($a, $b); + } + + public static function fromIntsWithDefault(int $a, int $b) + { + return new self($a, $b); + } + + public static function fromSimpleDara(SimpleData $data) + { + return new self($data->string, $data->string); + } + + public static function fromData(Data $data) + { + return new self('data', json_encode($data)); + } + }; + + expect($dataClass::from('Hello'))->toEqual(new $dataClass('Hello', 'World')) + ->and($dataClass::from('Hello', 'World'))->toEqual(new $dataClass('Hello', 'World')) + ->and($dataClass::from(42, 69))->toEqual(new $dataClass(42, 69)) + ->and($dataClass::from(SimpleData::from('Hello')))->toEqual(new $dataClass('Hello', 'Hello')) + ->and($dataClass::from(new EnumData(DummyBackedEnum::FOO)))->toEqual(new $dataClass('data', '{"enum":"foo"}')); +}); + +it('can create data using a magical method with the interface of the value as type', function () { $data = new class ('') extends Data { public function __construct(public string $string) { @@ -64,7 +106,7 @@ public function toArray() expect($data::from($interfaceable))->toEqual(new $data('Rick Astley')); }); -it('can create data from a custom method with an inherit parameter', function () { +it('can create data using a magical method with the base class of the value as type', function () { $data = new class ('') extends Data { public function __construct(public string $string) { @@ -81,12 +123,12 @@ public static function fromModel(Model $model) expect($data::from($inherited))->toEqual(new $data('Rick Astley')); }); -it('can create data from a custom method with multiple parameters', function () { +it('can create data from a magical method with multiple parameters', function () { expect(DataWithMultipleArgumentCreationMethod::from('Rick Astley', 42)) ->toEqual(new DataWithMultipleArgumentCreationMethod('Rick Astley_42')); }); -it('can create data without custom creation methods', function () { +it('can disable the use of magical methods', function () { $data = new class ('', '') extends Data { public function __construct( public ?string $id, @@ -145,3 +187,174 @@ public static function fromArray(array $payload) ) )->toEqual(new DummyA(1, 'Taylor')); }); + +it('can inject the creation context when using a magical method', function () { + $dataClass = new class extends Data { + public function __construct( + public string $string = 'something' + ) { + } + + public static function fromArray(string $prefix, CreationContext $context) + { + return new self("{$prefix} {$context->dataClass}"); + } + }; + + expect($dataClass::from('Hi there')) + ->string->toBe('Hi there '.$dataClass::class); +}); + +it('will use magic methods when creating a collection of data objects', function () { + $dataClass = new class ('') extends Data { + public function __construct(public string $otherString) + { + } + + public static function fromSimpleData(SimpleData $simpleData): static + { + return new self($simpleData->string); + } + }; + + $collection = new DataCollection($dataClass::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]); + + expect($collection[0]) + ->toBeInstanceOf($dataClass::class) + ->otherString->toEqual('A'); + + expect($collection[1]) + ->toBeInstanceOf($dataClass::class) + ->otherString->toEqual('B'); +}); + +it('can magically collect data', function () { + class TestSomeCustomCollection extends Collection + { + } + + $dataClass = new class () extends Data { + public string $string; + + public static function fromString(string $string): self + { + $s = new self(); + + $s->string = $string; + + return $s; + } + + public static function collectArray(array $items): \TestSomeCustomCollection + { + return new \TestSomeCustomCollection($items); + } + + public static function collectCollection(Collection $items): array + { + return $items->all(); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(\TestSomeCustomCollection::class) + ->all()->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); + + expect($dataClass::collect(collect(['a', 'b', 'c']))) + ->toBeArray() + ->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); + + expect($dataClass::collect(new TestSomeCustomCollection(['a', 'b', 'c']))) + ->toBeArray() + ->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); +}); + +it('can disable magically collecting data', function () { + $dataClass = new class ('') extends SimpleData { + public static function collectArray(array $items): Collection + { + return new Collection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(Collection::class) + ->all()->toEqual([ + SimpleData::from('a'), + SimpleData::from('b'), + SimpleData::from('c'), + ]); + + expect($dataClass::factory()->withoutMagicalCreation()->collect([ + ['string' => 'a'], + ['string' => 'b'], + ['string' => 'c'] + ])) + ->toBeArray() + ->toEqual([ + new $dataClass('a'), + new $dataClass('b'), + new $dataClass('c'), + ]); +}); + +it('can disable specific magic collecting data methods', function () { + $dataClass = new class ('') extends SimpleData { + public static function collectArray(array $items): Collection + { + return new Collection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(Collection::class) + ->all()->toEqual([ + SimpleData::from('a'), + SimpleData::from('b'), + SimpleData::from('c'), + ]); + + expect($dataClass::factory()->ignoreMagicalMethod('collectArray')->collect([ + ['string' => 'a'], + ['string' => 'b'], + ['string' => 'c'] + ])) + ->toBeArray() + ->toEqual([ + new $dataClass('a'), + new $dataClass('b'), + new $dataClass('c'), + ]); +}); + +it('can inject the creation context when collecting data with a magical method', function (){ + $dataClass = new class ('') extends SimpleData { + public static function collectArray(array $items, CreationContext $context): array + { + return array_map(fn(SimpleData $data) => new SimpleData($data->string . ' ' . $context->dataClass), $items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeArray() + ->toEqual([ + SimpleData::from('a ' . $dataClass::class), + SimpleData::from('b ' . $dataClass::class), + SimpleData::from('c ' . $dataClass::class), + ]); +}); diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index cf7c1022..bd23f78d 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -1,15 +1,19 @@ get()) extends Data { public function __construct( string $nonPromoted, public $withoutType, public string $property, + CreationContext $creationContext, public string $propertyWithDefault = 'hello', ) { } @@ -23,7 +27,8 @@ public function __construct( ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) + ->isCreationContext->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'withoutType'); $parameter = DataParameter::create($reflection, $class::class); @@ -33,7 +38,8 @@ public function __construct( ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) + ->isCreationContext->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'property'); $parameter = DataParameter::create($reflection, $class::class); @@ -43,7 +49,19 @@ public function __construct( ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) + ->isCreationContext->toBeFalse(); + + $reflection = new ReflectionParameter([$class::class, '__construct'], 'creationContext'); + $parameter = DataParameter::create($reflection, $class::class); + + expect($parameter) + ->name->toEqual('creationContext') + ->isPromoted->toBeFalse() + ->hasDefaultValue->toBeFalse() + ->defaultValue->toBeNull() + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) + ->isCreationContext->toBeTrue(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'propertyWithDefault'); $parameter = DataParameter::create($reflection, $class::class); @@ -53,5 +71,6 @@ public function __construct( ->isPromoted->toBeTrue() ->hasDefaultValue->toBeTrue() ->defaultValue->toEqual('hello') - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)); + ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) + ->isCreationContext->toBeFalse(); }); From 6d8b6ad11bffd8261a462d9a7948543ecbe1511a Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 16 Jan 2024 14:06:34 +0000 Subject: [PATCH 075/124] Fix styling --- src/Concerns/BaseData.php | 2 +- src/Support/DataMethod.php | 2 +- tests/CreationTest.php | 1 - tests/MagicalCreationTest.php | 10 +++++----- tests/TransformationTest.php | 4 +--- 5 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 93250213..3c7b95fc 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -100,7 +100,7 @@ public function __sleep(): array return $dataClass ->properties ->map(fn (DataProperty $property) => $property->name) - ->when($dataClass->appendable, fn(Collection $properties) => $properties->push('_additional')) + ->when($dataClass->appendable, fn (Collection $properties) => $properties->push('_additional')) ->toArray(); } } diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 6ccca9d4..fac973ee 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -106,7 +106,7 @@ public function accepts(mixed ...$input): bool : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); $parameters = $parameters->reject( - fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->isCreationContext + fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->isCreationContext ); if (count($input) > $parameters->count()) { diff --git a/tests/CreationTest.php b/tests/CreationTest.php index 7893413f..44e44d33 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -4,7 +4,6 @@ use Carbon\CarbonImmutable; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Pagination\LengthAwarePaginator; -use Illuminate\Support\Collection; use Illuminate\Validation\ValidationException; use Spatie\LaravelData\Attributes\Computed; use Spatie\LaravelData\Attributes\DataCollectionOf; diff --git a/tests/MagicalCreationTest.php b/tests/MagicalCreationTest.php index b894a2c8..045c9781 100644 --- a/tests/MagicalCreationTest.php +++ b/tests/MagicalCreationTest.php @@ -189,7 +189,7 @@ public static function fromArray(array $payload) }); it('can inject the creation context when using a magical method', function () { - $dataClass = new class extends Data { + $dataClass = new class () extends Data { public function __construct( public string $string = 'something' ) { @@ -303,7 +303,7 @@ public static function collectArray(array $items): Collection expect($dataClass::factory()->withoutMagicalCreation()->collect([ ['string' => 'a'], ['string' => 'b'], - ['string' => 'c'] + ['string' => 'c'], ])) ->toBeArray() ->toEqual([ @@ -332,7 +332,7 @@ public static function collectArray(array $items): Collection expect($dataClass::factory()->ignoreMagicalMethod('collectArray')->collect([ ['string' => 'a'], ['string' => 'b'], - ['string' => 'c'] + ['string' => 'c'], ])) ->toBeArray() ->toEqual([ @@ -342,11 +342,11 @@ public static function collectArray(array $items): Collection ]); }); -it('can inject the creation context when collecting data with a magical method', function (){ +it('can inject the creation context when collecting data with a magical method', function () { $dataClass = new class ('') extends SimpleData { public static function collectArray(array $items, CreationContext $context): array { - return array_map(fn(SimpleData $data) => new SimpleData($data->string . ' ' . $context->dataClass), $items); + return array_map(fn (SimpleData $data) => new SimpleData($data->string . ' ' . $context->dataClass), $items); } }; diff --git a/tests/TransformationTest.php b/tests/TransformationTest.php index 84f0aca2..0dd8c7e0 100644 --- a/tests/TransformationTest.php +++ b/tests/TransformationTest.php @@ -12,13 +12,10 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; -use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Fakes\CircData; -use Spatie\LaravelData\Tests\Fakes\CollectionAnnotationsData; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -28,6 +25,7 @@ use Spatie\LaravelData\Tests\Fakes\UlarData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; use Spatie\LaravelData\Transformers\Transformer; + use function Spatie\Snapshots\assertMatchesJsonSnapshot; it('can transform a data object', function () { From 3597df045ac6c4cc04e925728f871523fc9a9d2d Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 16 Jan 2024 17:22:44 +0100 Subject: [PATCH 076/124] Better testing --- src/Concerns/ContextableData.php | 16 +- src/Resolvers/TransformedDataResolver.php | 5 + src/Resolvers/VisibleDataFieldsResolver.php | 21 +- .../Partials/ResolvedPartialsCollection.php | 6 +- .../Transformation/TransformationContext.php | 58 +- .../TransformationContextFactory.php | 52 +- tests/PartialsTest.php | 650 +------------- .../VisibleDataFieldsResolverTest.php | 794 +++++++++++++++++- 8 files changed, 878 insertions(+), 724 deletions(-) diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php index 9310e52a..3feb8848 100644 --- a/src/Concerns/ContextableData.php +++ b/src/Concerns/ContextableData.php @@ -22,26 +22,26 @@ public function getDataContext(): DataContext default => new Wrap(WrapType::UseGlobal), }; - $includedPartials = null; - $excludedPartials = null; + $includePartials = null; + $excludePartials = null; $onlyPartials = null; $exceptPartials = null; if ($this instanceof IncludeableDataContract) { if (! empty($this->includeProperties())) { - $includedPartials = new PartialsCollection(); + $includePartials = new PartialsCollection(); } foreach ($this->includeProperties() as $key => $value) { - $includedPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); + $includePartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); } if (! empty($this->excludeProperties())) { - $excludedPartials = new PartialsCollection(); + $excludePartials = new PartialsCollection(); } foreach ($this->excludeProperties() as $key => $value) { - $excludedPartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); + $excludePartials->attach(Partial::fromMethodDefinedKeyAndValue($key, $value)); } if (! empty($this->onlyProperties())) { @@ -62,8 +62,8 @@ public function getDataContext(): DataContext } return $this->_dataContext = new DataContext( - $includedPartials, - $excludedPartials, + $includePartials, + $excludePartials, $onlyPartials, $exceptPartials, $this instanceof WrappableDataContract ? $wrap : new Wrap(WrapType::UseGlobal), diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 145b7c71..57abadbd 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataContainer; @@ -69,6 +70,10 @@ private function transform( $visibleFields[$name] ?? null, ); + if($value instanceof Optional){ + continue; + } + if ($context->mapPropertyNames && $property->outputMappedName) { $name = $property->outputMappedName; } diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index 9d71dda7..fca33d20 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -56,13 +56,13 @@ public function execute( $this->performOnly($fields, $transformationContext); } - $includedFields = $transformationContext->includedPartials ? $this->resolveIncludedFields( + $includedFields = $transformationContext->includePartials ? $this->resolveIncludedFields( $fields, $transformationContext, $dataClass, ) : []; - $excludedFields = $transformationContext->excludedPartials ? $this->resolveExcludedFields( + $excludedFields = $transformationContext->excludePartials ? $this->resolveExcludedFields( $fields, $transformationContext, $dataClass, @@ -193,7 +193,7 @@ protected function resolveIncludedFields( ): array { $includedFields = []; - foreach ($transformationContext->includedPartials as $includedPartial) { + foreach ($transformationContext->includePartials as $includedPartial) { if ($includedPartial->isUndefined()) { continue; } @@ -238,12 +238,12 @@ protected function resolveExcludedFields( ): array { $excludedFields = []; - foreach ($transformationContext->excludedPartials as $excludedPartial) { - if ($excludedPartial->isUndefined()) { + foreach ($transformationContext->excludePartials as $excludePartial) { + if ($excludePartial->isUndefined()) { continue; } - if ($excludedPartial->isAll()) { + if ($excludePartial->isAll()) { $excludedFields = $dataClass ->properties ->filter(fn (DataProperty $property) => $property->type->lazyType !== null && array_key_exists($property->name, $fields)) @@ -251,20 +251,19 @@ protected function resolveExcludedFields( ->all(); foreach ($excludedFields as $excludedField) { - $fields[$excludedField]?->addExcludedResolvedPartial($excludedPartial->next()); + $fields[$excludedField]?->addExcludedResolvedPartial($excludePartial->next()); } break; } - if ($nested = $excludedPartial->getNested()) { - $fields[$nested]->addExcludedResolvedPartial($excludedPartial->next()); - $excludedFields[] = $nested; + if ($nested = $excludePartial->getNested()) { + $fields[$nested]->addExcludedResolvedPartial($excludePartial->next()); continue; } - if ($selectedFields = $excludedPartial->getFields()) { + if ($selectedFields = $excludePartial->getFields()) { array_push($excludedFields, ...$selectedFields); } } diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php index b1c10ac7..6324cdb8 100644 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -34,10 +34,10 @@ public function toArray(): array public function __toString(): string { - $output = "- excludedPartials:".PHP_EOL; + $output = "- partials:".PHP_EOL; - foreach ($this as $excludedPartial) { - $output .= " - {$excludedPartial}".PHP_EOL; + foreach ($this as $partial) { + $output .= " - {$partial}".PHP_EOL; } return $output; diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index fa285015..da735de8 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -20,8 +20,8 @@ public function __construct( public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, public ?GlobalTransformersCollection $transformers = null, - public ?ResolvedPartialsCollection $includedPartials = null, - public ?ResolvedPartialsCollection $excludedPartials = null, + public ?ResolvedPartialsCollection $includePartials = null, + public ?ResolvedPartialsCollection $excludePartials = null, public ?ResolvedPartialsCollection $onlyPartials = null, public ?ResolvedPartialsCollection $exceptPartials = null, ) { @@ -36,23 +36,23 @@ public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self public function addIncludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void { - if ($this->includedPartials === null) { - $this->includedPartials = new ResolvedPartialsCollection(); + if ($this->includePartials === null) { + $this->includePartials = new ResolvedPartialsCollection(); } foreach ($resolvedPartials as $resolvedPartial) { - $this->includedPartials->attach($resolvedPartial); + $this->includePartials->attach($resolvedPartial); } } public function addExcludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void { - if ($this->excludedPartials === null) { - $this->excludedPartials = new ResolvedPartialsCollection(); + if ($this->excludePartials === null) { + $this->excludePartials = new ResolvedPartialsCollection(); } foreach ($resolvedPartials as $resolvedPartial) { - $this->excludedPartials->attach($resolvedPartial); + $this->excludePartials->attach($resolvedPartial); } } @@ -80,20 +80,20 @@ public function addExceptResolvedPartial(ResolvedPartial ...$resolvedPartials): public function mergeIncludedResolvedPartials(ResolvedPartialsCollection $partials): void { - if ($this->includedPartials === null) { - $this->includedPartials = new ResolvedPartialsCollection(); + if ($this->includePartials === null) { + $this->includePartials = new ResolvedPartialsCollection(); } - $this->includedPartials->addAll($partials); + $this->includePartials->addAll($partials); } public function mergeExcludedResolvedPartials(ResolvedPartialsCollection $partials): void { - if ($this->excludedPartials === null) { - $this->excludedPartials = new ResolvedPartialsCollection(); + if ($this->excludePartials === null) { + $this->excludePartials = new ResolvedPartialsCollection(); } - $this->excludedPartials->addAll($partials); + $this->excludePartials->addAll($partials); } public function mergeOnlyResolvedPartials(ResolvedPartialsCollection $partials): void @@ -116,15 +116,15 @@ public function mergeExceptResolvedPartials(ResolvedPartialsCollection $partials public function rollBackPartialsWhenRequired(): void { - if ($this->includedPartials !== null) { - foreach ($this->includedPartials as $includedPartial) { + if ($this->includePartials !== null) { + foreach ($this->includePartials as $includedPartial) { $includedPartial->rollbackWhenRequired(); } } - if ($this->excludedPartials !== null) { - foreach ($this->excludedPartials as $excludedPartial) { - $excludedPartial->rollbackWhenRequired(); + if ($this->excludePartials !== null) { + foreach ($this->excludePartials as $excludePartial) { + $excludePartial->rollbackWhenRequired(); } } @@ -183,8 +183,8 @@ public function toArray(): array 'mapPropertyNames' => $this->mapPropertyNames, 'wrapExecutionType' => $this->wrapExecutionType, 'transformers' => $this->transformers !== null ? iterator_to_array($this->transformers) : null, - 'includedPartials' => $this->includedPartials?->toArray(), - 'excludedPartials' => $this->excludedPartials?->toArray(), + 'includePartials' => $this->includePartials?->toArray(), + 'excludePartials' => $this->excludePartials?->toArray(), 'onlyPartials' => $this->onlyPartials?->toArray(), 'exceptPartials' => $this->exceptPartials?->toArray(), ]; @@ -192,12 +192,12 @@ public function toArray(): array public function __clone(): void { - if ($this->includedPartials !== null) { - $this->includedPartials = clone $this->includedPartials; + if ($this->includePartials !== null) { + $this->includePartials = clone $this->includePartials; } - if ($this->excludedPartials !== null) { - $this->excludedPartials = clone $this->excludedPartials; + if ($this->excludePartials !== null) { + $this->excludePartials = clone $this->excludePartials; } if ($this->onlyPartials !== null) { @@ -223,12 +223,12 @@ public function __toString(): string $output .= "- mapPropertyNames: true".PHP_EOL; } - if ($this->includedPartials !== null && $this->includedPartials->count() > 0) { - $output .= $this->includedPartials; + if ($this->includePartials !== null && $this->includePartials->count() > 0) { + $output .= $this->includePartials; } - if ($this->excludedPartials !== null && $this->excludedPartials->count() > 0) { - $output .= $this->excludedPartials; + if ($this->excludePartials !== null && $this->excludePartials->count() > 0) { + $output .= $this->excludePartials; } if ($this->onlyPartials !== null && $this->onlyPartials->count() > 0) { diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index d1ec4f21..f53ce067 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -25,8 +25,8 @@ protected function __construct( public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, public ?GlobalTransformersCollection $transformers = null, - public ?PartialsCollection $includedPartials = null, - public ?PartialsCollection $excludedPartials = null, + public ?PartialsCollection $includePartials = null, + public ?PartialsCollection $excludePartials = null, public ?PartialsCollection $onlyPartials = null, public ?PartialsCollection $exceptPartials = null, ) { @@ -35,30 +35,30 @@ protected function __construct( public function get( BaseData|BaseDataCollectable $data, ): TransformationContext { - $includedPartials = null; + $includePartials = null; - if ($this->includedPartials) { - $includedPartials = new ResolvedPartialsCollection(); + if ($this->includePartials) { + $includePartials = new ResolvedPartialsCollection(); - foreach ($this->includedPartials as $include) { + foreach ($this->includePartials as $include) { $resolved = $include->resolve($data); if ($resolved) { - $includedPartials->attach($resolved); + $includePartials->attach($resolved); } } } - $excludedPartials = null; + $excludePartials = null; - if ($this->excludedPartials) { - $excludedPartials = new ResolvedPartialsCollection(); + if ($this->excludePartials) { + $excludePartials = new ResolvedPartialsCollection(); - foreach ($this->excludedPartials as $exclude) { + foreach ($this->excludePartials as $exclude) { $resolved = $exclude->resolve($data); if ($resolved) { - $excludedPartials->attach($resolved); + $excludePartials->attach($resolved); } } } @@ -96,8 +96,8 @@ public function get( $this->mapPropertyNames, $this->wrapExecutionType, $this->transformers, - $includedPartials, - $excludedPartials, + $includePartials, + $excludePartials, $onlyPartials, $exceptPartials, ); @@ -137,12 +137,12 @@ public function transformer(string $transformable, Transformer $transformer): st public function addIncludePartial(Partial ...$partial): static { - if ($this->includedPartials === null) { - $this->includedPartials = new PartialsCollection(); + if ($this->includePartials === null) { + $this->includePartials = new PartialsCollection(); } foreach ($partial as $include) { - $this->includedPartials->attach($include); + $this->includePartials->attach($include); } return $this; @@ -150,12 +150,12 @@ public function addIncludePartial(Partial ...$partial): static public function addExcludePartial(Partial ...$partial): static { - if ($this->excludedPartials === null) { - $this->excludedPartials = new PartialsCollection(); + if ($this->excludePartials === null) { + $this->excludePartials = new PartialsCollection(); } foreach ($partial as $exclude) { - $this->excludedPartials->attach($exclude); + $this->excludePartials->attach($exclude); } return $this; @@ -189,22 +189,22 @@ public function addExceptPartial(Partial ...$partial): static public function mergeIncludePartials(PartialsCollection $partials): static { - if ($this->includedPartials === null) { - $this->includedPartials = new PartialsCollection(); + if ($this->includePartials === null) { + $this->includePartials = new PartialsCollection(); } - $this->includedPartials->addAll($partials); + $this->includePartials->addAll($partials); return $this; } public function mergeExcludePartials(PartialsCollection $partials): static { - if ($this->excludedPartials === null) { - $this->excludedPartials = new PartialsCollection(); + if ($this->excludePartials === null) { + $this->excludePartials = new PartialsCollection(); } - $this->excludedPartials->addAll($partials); + $this->excludePartials->addAll($partials); return $this; } diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 9cc3f93d..9450c3ac 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -29,258 +29,10 @@ use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedOutputName; use Spatie\LaravelData\Tests\Fakes\UlarData; -it('can include a lazy property', function () { - $data = new LazyData(Lazy::create(fn () => 'test')); - - expect($data->toArray())->toBe([]); - - expect($data->include('name')->toArray()) - ->toMatchArray([ - 'name' => 'test', - ]); -}); - -it('can have a prefilled in lazy property', function () { - $data = new LazyData('test'); - - expect($data->toArray())->toMatchArray([ - 'name' => 'test', - ]); - - expect($data->include('name')->toArray()) - ->toMatchArray([ - 'name' => 'test', - ]); -}); - -it('can include a nested lazy property', function () { - class TestIncludeableNestedLazyDataProperties extends Data - { - public function __construct( - public LazyData|Lazy $data, - #[DataCollectionOf(LazyData::class)] - public array|Lazy $collection, - ) { - } - } - - $data = new \TestIncludeableNestedLazyDataProperties( - Lazy::create(fn () => LazyData::from('Hello')), - Lazy::create(fn () => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), - ); - - expect((clone $data)->toArray())->toBe([]); - - expect((clone $data)->include('data')->toArray())->toMatchArray([ - 'data' => [], - ]); - - expect((clone $data)->include('data.name')->toArray())->toMatchArray([ - 'data' => ['name' => 'Hello'], - ]); - - expect((clone $data)->include('collection')->toArray())->toMatchArray([ - 'collection' => [ - [], - [], - [], - [], - [], - [], - ], - ]); - - expect((clone $data)->include('collection.name')->toArray())->toMatchArray([ - 'collection' => [ - ['name' => 'is'], - ['name' => 'it'], - ['name' => 'me'], - ['name' => 'your'], - ['name' => 'looking'], - ['name' => 'for'], - ], - ]); -}); - -it('can include specific nested data collections', function () { - class TestSpecificDefinedIncludeableCollectedAndNestedLazyData extends Data - { - public function __construct( - #[DataCollectionOf(MultiLazyData::class)] - public array|Lazy $songs - ) { - } - } - - $collection = Lazy::create(fn () => MultiLazyData::collect([ - DummyDto::rick(), - DummyDto::bon(), - ])); - - $data = new \TestSpecificDefinedIncludeableCollectedAndNestedLazyData($collection); - - expect($data->include('songs.name')->toArray())->toMatchArray([ - 'songs' => [ - ['name' => DummyDto::rick()->name], - ['name' => DummyDto::bon()->name], - ], - ]); - - expect($data->include('songs.{name,artist}')->toArray())->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - ], - ], - ]); - - expect($data->include('songs.*')->toArray())->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - 'year' => DummyDto::rick()->year, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - 'year' => DummyDto::bon()->year, - ], - ], - ]); -}); - -it('can have a conditional lazy data', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|Lazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::when(fn () => $name === 'Ruben', fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->toArray())->toBe([]); - - $data = $blueprint::create('Ruben'); - - expect($data->toArray())->toMatchArray(['name' => 'Ruben']); -}); - -it('cannot have conditional lazy data manually loaded', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|Lazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::when(fn () => $name === 'Ruben', fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->include('name')->toArray())->toBeEmpty(); -}); - -it('can include data based upon relations loaded', function () { - $model = FakeNestedModel::factory()->create(); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); - - expect($transformed)->not->toHaveKey('fake_model'); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); - - expect($transformed)->toHaveKey('fake_model') - ->and($transformed['fake_model'])->toBeInstanceOf(FakeModelData::class); -}); - -it('can include data based upon relations loaded when they are null', function () { - $model = FakeNestedModel::factory(['fake_model_id' => null])->create(); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); - - expect($transformed)->not->toHaveKey('fake_model'); - - $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); - - expect($transformed)->toHaveKey('fake_model') - ->and($transformed['fake_model'])->toBeNull(); -}); - -it('can have default included lazy data', function () { - $data = new class ('Freek') extends Data { - public function __construct(public string|Lazy $name) - { - } - }; - - expect($data->toArray())->toMatchArray(['name' => 'Freek']); -}); - -it('can exclude default lazy data', function () { - $data = DefaultLazyData::from('Freek'); - - expect($data->exclude('name')->toArray())->toBe([]); -}); - -it('always transforms lazy inertia data to inertia lazy props', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|InertiaLazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::inertia(fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->toArray()['name'])->toBeInstanceOf(LazyProp::class); -}); - -it('always transforms closure lazy into closures for inertia', function () { - $blueprint = new class () extends Data { - public function __construct( - public string|ClosureLazy|null $name = null - ) { - } - - public static function create(string $name): static - { - return new self( - Lazy::closure(fn () => $name) - ); - } - }; - - $data = $blueprint::create('Freek'); - - expect($data->toArray()['name'])->toBeInstanceOf(Closure::class); -}); - +/** + * @note these are more "special" partial tests, like including via request, conditions, ... + * for unit tests of the partials themselves, see VisibleDataFieldsResolverTest + */ it('can dynamically include data based upon the request', function () { LazyData::setAllowedIncludes([]); @@ -437,30 +189,6 @@ public static function create(string $name): static }); -it('will not include lazy optional values when transforming', function () { - $data = new class ('Hello World', Lazy::create(fn () => Optional::create())) extends Data { - public function __construct( - public string $string, - public string|Optional|Lazy $lazy_optional_string, - ) { - } - }; - - expect(($data)->include('lazy_optional_string')->toArray())->toMatchArray([ - 'string' => 'Hello World', - ]); -}); - -it('excludes optional values data', function () { - $dataClass = new class () extends Data { - public string|Optional $name; - }; - - $data = $dataClass::from([]); - - expect($data->toArray())->toBe([]); -}); - it('can conditionally include', function () { expect( MultiLazyData::from(DummyDto::rick())->includeWhen('artist', false)->toArray() @@ -1253,123 +981,6 @@ protected function includeProperties(): array ]); }); -it('can combine multiple partials', function ( - array $include, - array $exclude, - array $only, - array $except, - array $expected -) { - $dataClass = new class ( - Lazy::create(fn () => NestedLazyData::from('Hello World')), - Lazy::create(fn () => NestedLazyData::collect(['Hello', 'World'])), - Lazy::create(fn () => SimpleData::from('Hello World')), - Lazy::create(fn () => MultiLazyData::from('Hello', 'World', 42)), - Lazy::create(fn () => 'Hello World')->defaultIncluded(), - ) extends Data { - public function __construct( - public Lazy|NestedLazyData $nested, - #[DataCollectionOf(NestedLazyData::class)] - public Lazy|array $collection, - public Lazy|SimpleData $simple, - public Lazy|MultiLazyData $multi, - public Lazy|string $string, - ) { - } - }; - - $array = $dataClass->include(...$include)->exclude(...$exclude)->only(...$only)->except(...$except)->toArray(); - - expect($array)->toMatchArray($expected); -})->with(function () { - yield 'no includes' => [ - 'include' => [], - 'exclude' => [], - 'only' => [], - 'except' => [], - 'expected' => [ - 'string' => 'Hello World', - ], - ]; - - yield 'include and exclude' => [ - 'include' => ['simple'], - 'exclude' => ['string'], - 'only' => [], - 'except' => [], - 'expected' => [ - 'simple' => ['string' => 'Hello World'], - ], - ]; - - yield 'combined include' => [ - 'include' => ['multi.*', 'simple', 'collection.*'], - 'exclude' => [], - 'only' => [], - 'except' => [], - 'expected' => [ - 'collection' => [ - ['simple' => ['string' => 'Hello']], - ['simple' => ['string' => 'World']], - ], - 'simple' => ['string' => 'Hello World'], - 'multi' => [ - 'artist' => 'Hello', - 'name' => 'World', - 'year' => 42, - ], - 'string' => 'Hello World', - ], - ]; - - yield 'included similar paths' => [ - 'include' => ['multi.artist', 'multi.name'], - 'exclude' => [], - 'only' => [], - 'except' => [], - 'expected' => [ - 'multi' => [ - 'artist' => 'Hello', - 'name' => 'World', - ], - 'string' => 'Hello World', - ], - ]; - - yield 'include all' => [ - 'include' => ['*'], - 'exclude' => [], - 'only' => [], - 'except' => [], - 'expected' => [ - 'collection' => [ - ['simple' => ['string' => 'Hello']], - ['simple' => ['string' => 'World']], - ], - 'simple' => ['string' => 'Hello World'], - 'multi' => [ - 'artist' => 'Hello', - 'name' => 'World', - 'year' => 42, - ], - 'string' => 'Hello World', - ], - ]; - - yield 'except and only' => [ - 'include' => ['multi.*'], - 'exclude' => [], - 'only' => ['multi.{artist,name}'], - 'except' => ['multi.year'], - 'expected' => [ - 'multi' => [ - 'artist' => 'Hello', - 'name' => 'World', - ], - ], - ]; -}); - it('can set partials on a nested data object and these will be respected', function () { class TestMultiLazyNestedDataWithObjectAndCollection extends Data { @@ -1444,62 +1055,6 @@ public function __construct( ]); }); -it('can use only when transforming', function (array $directive, array $expectedOnly) { - $dataClass = new class () extends Data { - public string $first; - - public string $second; - - public MultiData $nested; - - #[DataCollectionOf(MultiData::class)] - public DataCollection $collection; - }; - - $data = $dataClass::from([ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ]); - - expect($data->only(...$directive)) - ->toArray() - ->toMatchArray($expectedOnly); -})->with('only-inclusion'); - -it('can use except when transforming', function ( - array $directive, - array $expectedOnly, - array $expectedExcept -) { - $dataClass = new class () extends Data { - public string $first; - - public string $second; - - public MultiData $nested; - - #[DataCollectionOf(MultiData::class)] - public DataCollection $collection; - }; - - $data = $dataClass::from([ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ]); - - expect($data->except(...$directive)->toArray()) - ->toEqual($expectedExcept); -})->with('only-inclusion'); // Todo: replace //it('will correctly reduce a tree based upon allowed includes', function ( @@ -1670,7 +1225,7 @@ public static function allowedRequestIncludes(): ?array ]); }); -it('handles parsing includes from request', function (array $input, array $expected) { +it('handles parsing includes from request in different formats', function (array $input, array $expected) { $dataclass = new class ( Lazy::create(fn () => 'Rick Astley'), Lazy::create(fn () => 'Never gonna give you up'), @@ -1747,198 +1302,3 @@ public static function allowedRequestIncludes(): ?array // Not really a test with expectation, we just want to check we don't end up in an infinite loop }); -dataset('only-inclusion', function () { - yield 'single' => [ - 'directive' => ['first'], - 'expectedOnly' => [ - 'first' => 'A', - ], - 'expectedExcept' => [ - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'multi' => [ - 'directive' => ['first', 'second'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - ], - 'expectedExcept' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'multi-2' => [ - 'directive' => ['{first,second}'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - ], - 'expectedExcept' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'all' => [ - 'directive' => ['*'], - 'expectedOnly' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [], - ]; - - yield 'nested' => [ - 'directive' => ['nested'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested.single' => [ - 'directive' => ['nested.first'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['second' => 'D'], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested.multi' => [ - 'directive' => ['nested.{first, second}'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => [], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'nested-all' => [ - 'directive' => ['nested.*'], - 'expectedOnly' => [ - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => [], - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - ]; - - yield 'collection' => [ - 'directive' => ['collection'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - ], - ]; - - yield 'collection-single' => [ - 'directive' => ['collection.first'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E'], - ['first' => 'G'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - ['second' => 'F'], - ['second' => 'H'], - ], - ], - ]; - - yield 'collection-multi' => [ - 'directive' => ['collection.first', 'collection.second'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - [], - [], - ], - ], - ]; - - yield 'collection-all' => [ - 'directive' => ['collection.*'], - 'expectedOnly' => [ - 'collection' => [ - ['first' => 'E', 'second' => 'F'], - ['first' => 'G', 'second' => 'H'], - ], - ], - 'expectedExcept' => [ - 'first' => 'A', - 'second' => 'B', - 'nested' => ['first' => 'C', 'second' => 'D'], - 'collection' => [ - [], - [], - ], - ], - ]; -}); diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index 1f5e2063..a74d735c 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -1,11 +1,15 @@ toEqual([ 'visible' => null, ]); + + expect($dataClass->toArray())->toBe([ + 'visible' => 'visible', + ]); }); it('will hide fields which are uninitialized', function () { @@ -48,8 +59,177 @@ function findVisibleFields( expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ 'visible' => null, ]); + + expect($dataClass->toArray())->toBe([ + 'visible' => 'visible', + ]); +}); + +it('will hide optional values', function () { + $dataClass = new class () extends Data { + public Lazy|string|Optional $lazyOptional; + + public function __construct( + public string $visible = 'visible', + public Optional|string $optional = new Optional(), + ) { + $this->lazyOptional = Lazy::create(fn () => new Optional()); + } + }; + + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ + 'visible' => null, + ]); + + expect($dataClass->include('lazyOptional')->toArray())->toBe([ + 'visible' => 'visible', + ]); +}); + +it('will always show non-lazy values when no only or exclude operations are performed on it', function () { + $dataClass = new class () extends Data { + public function __construct( + public string $visible = 'visible', + public Lazy|string $lazy = 'lazy but visible', + ) { + } + }; + + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ + 'visible' => null, + 'lazy' => null, + ]); + + expect($dataClass->toArray())->toBe([ + 'visible' => 'visible', + 'lazy' => 'lazy but visible', + ]); +}); + +it('can have lazy behaviour based upon a condition', function () { + $dataClass = new class () extends Data { + public function __construct( + public string|Lazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::when(fn () => $name === 'Ruben', fn () => $name) + ); + } + }; + + expect(findVisibleFields($dataClass, TransformationContextFactory::create()))->toEqual([ + 'name' => null, + ]); + + expect($dataClass::create('Freek')->toArray())->toBe([]); + expect($dataClass::create('Ruben')->toArray())->toMatchArray(['name' => 'Ruben']); +}); + +it('is impossible to lazy include conditional lazy properties', function () { + $dataClass = new class () extends Data { + public function __construct( + public string|Lazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::when(fn () => $name === 'Ruben', fn () => $name) + ); + } + }; + + $data = $dataClass::create('Freek')->include('name')->toArray(); + + expect($data)->toBeEmpty(); +}); + +it('can include data based upon relations being loaded', function () { + $model = FakeNestedModel::factory()->create(); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); + + expect($transformed)->not()->toHaveKey('fake_model'); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); + + expect($transformed) + ->toHaveKey('fake_model') + ->and($transformed['fake_model'])->toBeInstanceOf(FakeModelData::class); +}); + +it('can include data based upon relations loaded when they are null', function () { + $model = FakeNestedModel::factory(['fake_model_id' => null])->create(); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model)->all(); + + expect($transformed)->not()->toHaveKey('fake_model'); + + $transformed = FakeNestedModelData::createWithLazyWhenLoaded($model->load('fakeModel'))->all(); + + expect($transformed) + ->toHaveKey('fake_model') + ->and($transformed['fake_model'])->toBeNull(); +}); + +it('can include lazy data by default', function () { + $dataClass = new class ('') extends Data { + public function __construct( + public string|Lazy $name + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::create(fn () => $name)->defaultIncluded() + ); + } + }; + + expect($dataClass::create('Ruben')->toArray())->toMatchArray(['name' => 'Ruben']); +}); + +it('always transforms lazy inertia data to inertia lazy props', function () { + $dataClass = new class () extends Data { + public function __construct( + public string|InertiaLazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::inertia(fn () => $name) + ); + } + }; + + expect($dataClass::create('Freek')->toArray()['name'])->toBeInstanceOf(LazyProp::class); }); +it('always transforms closure lazy into closures for inertia', function () { + $dataClass = new class () extends Data { + public function __construct( + public string|ClosureLazy|null $name = null + ) { + } + + public static function create(string $name): static + { + return new self( + Lazy::closure(fn () => $name) + ); + } + }; + + expect($dataClass::create('Freek')->toArray()['name'])->toBeInstanceOf(Closure::class); +}); class VisibleFieldsSingleData extends Data { @@ -300,7 +480,7 @@ public static function instance(): self ), 'nested' => new TransformationContext( exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a') , new FieldsPartialSegment(['string'])], 1) + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) ), ), ], @@ -511,7 +691,7 @@ public static function instance(): self ), 'nested' => new TransformationContext( onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a') , new FieldsPartialSegment(['string'])], 1) + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) ), ), ], @@ -528,3 +708,613 @@ public static function instance(): self ], ]; }); + +class LazyVisibleFieldsSingleData extends Data +{ + public function __construct( + public Lazy|string $string, + public Lazy|int $int + ) { + } + + public static function instance(bool $includeByDefault): self + { + return new self( + Lazy::create(fn () => 'hello')->defaultIncluded($includeByDefault), + Lazy::create(fn () => 42)->defaultIncluded($includeByDefault) + ); + } +} + +class LazyVisibleFieldsNestedData extends Data +{ + public function __construct( + public Lazy|LazyVisibleFieldsSingleData $a, + public Lazy|LazyVisibleFieldsSingleData $b, + ) { + } + + public static function instance(bool $includeByDefault): self + { + return new self( + Lazy::create(fn () => LazyVisibleFieldsSingleData::instance($includeByDefault))->defaultIncluded($includeByDefault), + Lazy::create(fn () => LazyVisibleFieldsSingleData::instance($includeByDefault))->defaultIncluded($includeByDefault), + ); + } +} + +class LazyVisibleFieldsData extends Data +{ + public function __construct( + public Lazy|string $string, + public Lazy|int $int, + public Lazy|LazyVisibleFieldsSingleData $single, + public Lazy|LazyVisibleFieldsNestedData $nested, + #[DataCollectionOf(LazyVisibleFieldsSingleData::class)] + public Lazy|array $collection, + ) { + } + + public static function instance(bool $includeByDefault): self + { + return new self( + Lazy::create(fn () => 'hello')->defaultIncluded($includeByDefault), + Lazy::create(fn () => 42)->defaultIncluded($includeByDefault), + Lazy::create(fn () => LazyVisibleFieldsSingleData::instance($includeByDefault))->defaultIncluded($includeByDefault), + Lazy::create(fn () => LazyVisibleFieldsNestedData::instance($includeByDefault))->defaultIncluded($includeByDefault), + Lazy::create(fn () => [ + LazyVisibleFieldsSingleData::instance($includeByDefault), + LazyVisibleFieldsSingleData::instance($includeByDefault), + ])->defaultIncluded($includeByDefault), + ); + } +} + +it('can execute includes', function ( + TransformationContextFactory $factory, + array $expectedVisibleFields, + array $expectedTransformed +) { + $data = LazyVisibleFieldsData::instance(false); + + $visibleFields = findVisibleFields($data, $factory); + + $visibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $visibleFields); + $expectedVisibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $expectedVisibleFields); + + expect($visibleFields)->toEqual($expectedVisibleFields); + + expect($data->transform($factory))->toEqual($expectedTransformed); +})->with(function () { + yield 'single field' => [ + 'factory' => TransformationContextFactory::create() + ->include('single'), + 'fields' => [ + 'single' => new TransformationContext(), + ], + 'transformed' => [ + 'single' => [], + ], + ]; + + yield 'multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->include('{string,int,single}'), + 'fields' => [ + 'single' => new TransformationContext(), + 'int' => null, + 'string' => null, + ], + 'transformed' => [ + 'single' => [], + 'int' => 42, + 'string' => 'hello', + ], + ]; + + yield 'all' => [ + 'factory' => TransformationContextFactory::create() + ->include('*'), + 'fields' => [ + 'single' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new AllPartialSegment()], 3) + ), + ), + 'int' => null, + 'string' => null, + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new AllPartialSegment()], 3) + ), + ), + 'collection' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new AllPartialSegment()], 3) + ), + ), + ], + 'transformed' => [ + 'single' => ['string' => 'hello', 'int' => 42,], + 'int' => 42, + 'string' => 'hello', + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->include('nested.a'), + 'fields' => [ + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => [], + ], + ], + ]; + + yield 'nested data object multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->include('nested.{a,b}'), + 'fields' => [ + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => [], + 'b' => [], + ], + ], + ]; + + yield 'nested data object all' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->include('nested.*'), + 'fields' => [ + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object deep nesting' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->include('nested.a.string', 'nested.b.int'), + 'fields' => [ + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1), + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['string' => 'hello'], + 'b' => ['int' => 42], + ], + ], + ]; + + yield 'nested data collectable single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection') // ignore non collection fields + ->include('collection.string'), + 'fields' => [ + 'collection' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['string' => 'hello'], + ['string' => 'hello'], + ], + ], + ]; + + yield 'nested data collectable multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection') // ignore non collection fields + ->include('collection.{string,int}'), + 'fields' => [ + 'collection' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data collectable all' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection') // ignore non collection fields + ->include('collection.*'), + 'fields' => [ + 'collection' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'combination' => [ + 'factory' => TransformationContextFactory::create() + ->include('string', 'single.string') + ->include('collection.string') + ->include('nested.a.string'), + 'fields' => [ + 'string' => null, + 'single' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'collection' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'string' => 'hello', + 'single' => ['string' => 'hello'], + 'collection' => [ + ['string' => 'hello'], + ['string' => 'hello'], + ], + 'nested' => [ + 'a' => ['string' => 'hello'], + ], + ], + ]; +}); + +it('can execute excludes', function ( + TransformationContextFactory $factory, + array $expectedVisibleFields, + array $expectedTransformed +) { + $data = LazyVisibleFieldsData::instance(true); + + $visibleFields = findVisibleFields($data, $factory); + + $visibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $visibleFields); + $expectedVisibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $expectedVisibleFields); + + expect($visibleFields)->toEqual($expectedVisibleFields); + + expect($data->transform($factory))->toEqual($expectedTransformed); +})->with(function () { + yield 'single field' => [ + 'factory' => TransformationContextFactory::create() + ->exclude('single'), + 'fields' => [ + 'string' => null, + 'int' => null, + 'nested' => new TransformationContext(), + 'collection' => new TransformationContext(), + ], + 'transformed' => [ + 'string' => 'hello', + 'int' => 42, + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->exclude('{string,int,single}'), + 'fields' => [ + 'nested' => new TransformationContext(), + 'collection' => new TransformationContext(), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['string' => 'hello', 'int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + 'collection' => [ + ['string' => 'hello', 'int' => 42], + ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'all' => [ + 'factory' => TransformationContextFactory::create() + ->exclude('*'), + 'fields' => [], + 'transformed' => [], + ]; + + yield 'nested data object single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->exclude('nested.a'), + 'fields' => [ + 'nested' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'b' => ['string' => 'hello', 'int' => 42], + ], + ], + ]; + + yield 'nested data object multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->exclude('nested.{a,b}'), + 'fields' => [ + 'nested' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [], + ], + ]; + + yield 'nested data object all' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->exclude('nested.*'), + 'fields' => [ + 'nested' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [], + ], + ]; + + yield 'nested data object deep nesting' => [ + 'factory' => TransformationContextFactory::create() + ->only('nested') // ignore non nested object fields + ->exclude('nested.a.string', 'nested.b.int'), + 'fields' => [ + 'nested' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1), + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], 1) + ), + ), + ], + 'transformed' => [ + 'nested' => [ + 'a' => ['int' => 42], + 'b' => ['string' => 'hello'], + ], + ], + ]; + + yield 'nested data collectable single field' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection') // ignore non collection fields + ->exclude('collection.string'), + 'fields' => [ + 'collection' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + ['int' => 42], + ['int' => 42], + ], + ], + ]; + + yield 'nested data collectable multiple fields' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection') // ignore non collection fields + ->exclude('collection.{string,int}'), + 'fields' => [ + 'collection' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + [], + [], + ], + ], + ]; + + yield 'nested data collectable all' => [ + 'factory' => TransformationContextFactory::create() + ->only('collection') // ignore non collection fields + ->exclude('collection.*'), + 'fields' => [ + 'collection' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + ), + ), + ], + 'transformed' => [ + 'collection' => [ + [], + [], + ], + ], + ]; + + yield 'combination' => [ + 'factory' => TransformationContextFactory::create() + ->exclude('string', 'single.string') + ->exclude('collection.string') + ->exclude('nested.a.string'), + 'fields' => [ + 'single' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'collection' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'nested' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) + ), + ), + 'int' => null, + ], + 'transformed' => [ + 'single' => ['int' => 42], + 'collection' => [ + ['int' => 42], + ['int' => 42], + ], + 'nested' => [ + 'a' => ['int' => 42], + 'b' => ['string' => 'hello', 'int' => 42], + ], + 'int' => 42, + ], + ]; +}); + +it('can combine all the partials', function () { + $data = new LazyVisibleFieldsData( + Lazy::create(fn () => 'hello'), + Lazy::create(fn () => 42), + Lazy::create(fn () => LazyVisibleFieldsSingleData::instance(true))->defaultIncluded(), + Lazy::create(fn () => LazyVisibleFieldsNestedData::instance(false)), + Lazy::create(fn () => [ + LazyVisibleFieldsSingleData::instance(false), + LazyVisibleFieldsSingleData::instance(true), + ]), + ); + + $factory = TransformationContextFactory::create() + ->except('int', 'collection.int', 'nested.b.int') + ->only('single.*', 'nested.*', 'collection.*', 'string') + ->include('nested.a.string', 'nested.b.*', 'collection.string') + ->exclude('single.int'); + + $expectedVisibleFields = [ + 'single' => new TransformationContext( + excludePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['int'])], 1) + ), + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('single'), new AllPartialSegment()], 1) + ), + ), + 'nested' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1), + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new AllPartialSegment()], 1) + ), + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + ), + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], 1) + ), + ), + 'collection' => new TransformationContext( + includePartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + ), + onlyPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + ), + exceptPartials: ResolvedPartialsCollection::create( + new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['int'])], 1) + ), + ), + ]; + + $visibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, findVisibleFields($data, $factory)); + $expectedVisibleFields = array_map(fn ($field) => $field instanceof TransformationContext ? $field->toArray() : $field, $expectedVisibleFields); + + expect($visibleFields)->toEqual($expectedVisibleFields); + + expect($data->transform($factory))->toEqual([ + 'single' => ['string' => 'hello'], + 'nested' => [ + 'a' => ['string' => 'hello'], + 'b' => ['string' => 'hello'], + ], + 'collection' => [ + ['string' => 'hello'], + ['string' => 'hello'], + ], + ]); +}); From 49a184fc3b2b26fbdacf8a4a3019d3f41a1fab17 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 16 Jan 2024 16:23:20 +0000 Subject: [PATCH 077/124] Fix styling --- src/Resolvers/TransformedDataResolver.php | 2 +- tests/PartialsTest.php | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 57abadbd..38559eae 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -70,7 +70,7 @@ private function transform( $visibleFields[$name] ?? null, ); - if($value instanceof Optional){ + if($value instanceof Optional) { continue; } diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 9450c3ac..76bd5e72 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -2,23 +2,16 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; -use Inertia\LazyProp; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Lazy; -use Spatie\LaravelData\Optional; -use Spatie\LaravelData\Support\Lazy\ClosureLazy; -use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Tests\Fakes\CircData; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\ExceptData; -use Spatie\LaravelData\Tests\Fakes\FakeModelData; -use Spatie\LaravelData\Tests\Fakes\FakeNestedModelData; use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\Models\FakeModel; -use Spatie\LaravelData\Tests\Fakes\Models\FakeNestedModel; use Spatie\LaravelData\Tests\Fakes\MultiData; use Spatie\LaravelData\Tests\Fakes\MultiLazyData; use Spatie\LaravelData\Tests\Fakes\NestedLazyData; @@ -1301,4 +1294,3 @@ public static function allowedRequestIncludes(): ?array // Not really a test with expectation, we just want to check we don't end up in an infinite loop }); - From 0346ce45b68656279c079c7231284b3cae05af83 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 17 Jan 2024 14:56:54 +0100 Subject: [PATCH 078/124] wip --- UPGRADING.md | 1 + config/data.php | 8 +- phpstan-baseline.neon | 10 + src/Concerns/ResponsableData.php | 7 +- .../CannotPerformPartialOnDataField.php | 26 ++ .../RequestQueryStringPartialsResolver.php | 2 +- src/Resolvers/VisibleDataFieldsResolver.php | 66 +++- .../Creation/CreationContextFactory.php | 1 - src/Support/Partials/Partial.php | 9 + src/Support/Partials/PartialType.php | 10 + src/Support/Partials/PartialsCollection.php | 21 ++ .../Partials/ResolvedPartialsCollection.php | 2 +- .../Transformation/TransformationContext.php | 4 + tests/CreationFactoryTest.php | 150 ++++++++ tests/Fakes/Casts/MeaningOfLifeCast.php | 16 + tests/MagicalCreationTest.php | 62 ---- tests/PartialsTest.php | 334 ++++++++++-------- tests/RequestTest.php | 27 +- .../VisibleDataFieldsResolverTest.php | 35 ++ tests/ValidationTest.php | 37 +- 20 files changed, 579 insertions(+), 249 deletions(-) create mode 100644 src/Exceptions/CannotPerformPartialOnDataField.php create mode 100644 tests/CreationFactoryTest.php create mode 100644 tests/Fakes/Casts/MeaningOfLifeCast.php diff --git a/UPGRADING.md b/UPGRADING.md index a062bfa5..8460d00f 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -28,6 +28,7 @@ The following things are required when upgrading: - 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 +- In previous versions, when trying to include, exclude, only or except certain data properties that did not exist not exception was thrown. This is now the case, these exceptions can be silenced by setting `ignore_invalid_partials` to true within the config file We advise you to take a look at the following things: - Take a look within your data objects if `DataCollection`'s, `DataPaginatedCollection`'s and `DataCursorPaginatedCollection`'s can be replaced with regular arrays, Laravel Collections and Paginator diff --git a/config/data.php b/config/data.php index a1ae3c83..d0dca3e3 100644 --- a/config/data.php +++ b/config/data.php @@ -93,5 +93,11 @@ * method. By default, only when a request is passed the data is being validated. This * behaviour can be changed to always validate or to completely disable validation. */ - 'validation_type' => \Spatie\LaravelData\Support\Creation\ValidationType::OnlyRequests->value + 'validation_type' => \Spatie\LaravelData\Support\Creation\ValidationType::OnlyRequests->value, + + /** + * When using an invalid include, exclude, only or except partial, the package will + * throw an + */ + 'ignore_invalid_partials' => false, ]; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d314109d..5c688b1e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -65,6 +65,11 @@ parameters: count: 1 path: src/Casts/DateTimeInterfaceCast.php + - + message: "#^PHPDoc tag @return with type Spatie\\\\LaravelData\\\\CursorPaginatedDataCollection\\ is incompatible with native type static\\(Spatie\\\\LaravelData\\\\CursorPaginatedDataCollection\\\\)\\.$#" + count: 1 + path: src/CursorPaginatedDataCollection.php + - message: "#^Instanceof between \\*NEVER\\* and Spatie\\\\LaravelData\\\\Contracts\\\\BaseDataCollectable will always evaluate to false\\.$#" count: 1 @@ -95,6 +100,11 @@ parameters: count: 2 path: src/PaginatedDataCollection.php + - + message: "#^PHPDoc tag @return with type Spatie\\\\LaravelData\\\\PaginatedDataCollection\\ is incompatible with native type static\\(Spatie\\\\LaravelData\\\\PaginatedDataCollection\\\\)\\.$#" + count: 1 + path: src/PaginatedDataCollection.php + - message: "#^Dead catch \\- ArgumentCountError is never thrown in the try block\\.$#" count: 1 diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 68dfed0d..1371809f 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -59,10 +59,15 @@ public function toResponse($request) return new JsonResponse( data: $this->transform($contextFactory), - status: $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK, + status: $this->calculateResponseStatus($request), ); } + protected function calculateResponseStatus(Request $request): int + { + return $request->isMethod(Request::METHOD_POST) ? Response::HTTP_CREATED : Response::HTTP_OK; + } + public static function allowedRequestIncludes(): ?array { return []; diff --git a/src/Exceptions/CannotPerformPartialOnDataField.php b/src/Exceptions/CannotPerformPartialOnDataField.php new file mode 100644 index 00000000..15e47869 --- /dev/null +++ b/src/Exceptions/CannotPerformPartialOnDataField.php @@ -0,0 +1,26 @@ +getVerb()} a non existing field `{$field}` on `{$dataClass->name}`.".PHP_EOL; + $message .= 'Provided transformation context:'.PHP_EOL.PHP_EOL; + $message .= (string) $transformationContext; + + return new self(message: $message, previous: $exception); + } +} diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php index 7ae11958..7a69959c 100644 --- a/src/Resolvers/RequestQueryStringPartialsResolver.php +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -105,7 +105,7 @@ protected function validateSegments( ); if ($nextSegments === null) { - return [$segment]; + return [new FieldsPartialSegment([$field])]; } return [$segment, ...$nextSegments]; diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index fca33d20..5f7579e9 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -2,13 +2,16 @@ namespace Spatie\LaravelData\Resolvers; +use ErrorException; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Exceptions\CannotPerformPartialOnDataField; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; +use Spatie\LaravelData\Support\Partials\PartialType; use Spatie\LaravelData\Support\Transformation\TransformationContext; class VisibleDataFieldsResolver @@ -45,7 +48,7 @@ public function execute( } if ($transformationContext->exceptPartials) { - $this->performExcept($fields, $transformationContext); + $this->performExcept($fields, $transformationContext, $dataClass); } if (empty($fields)) { @@ -53,7 +56,7 @@ public function execute( } if ($transformationContext->onlyPartials) { - $this->performOnly($fields, $transformationContext); + $this->performOnly($fields, $transformationContext, $dataClass); } $includedFields = $transformationContext->includePartials ? $this->resolveIncludedFields( @@ -110,7 +113,8 @@ public function execute( */ protected function performExcept( array &$fields, - TransformationContext $transformationContext + TransformationContext $transformationContext, + DataClass $dataClass, ): void { $exceptFields = []; @@ -126,7 +130,11 @@ protected function performExcept( } if ($nested = $exceptPartial->getNested()) { - $fields[$nested]->addExceptResolvedPartial($exceptPartial->next()); + try { + $fields[$nested]->addExceptResolvedPartial($exceptPartial->next()); + } catch (ErrorException $exception) { + $this->handleNonExistingNestedField($exception, PartialType::Except, $nested, $dataClass, $transformationContext); + } continue; } @@ -146,7 +154,8 @@ protected function performExcept( */ protected function performOnly( array &$fields, - TransformationContext $transformationContext + TransformationContext $transformationContext, + DataClass $dataClass, ): void { $onlyFields = null; @@ -159,8 +168,12 @@ protected function performOnly( $onlyFields ??= []; if ($nested = $onlyPartial->getNested()) { - $fields[$nested]->addOnlyResolvedPartial($onlyPartial->next()); - $onlyFields[] = $nested; + try { + $fields[$nested]->addOnlyResolvedPartial($onlyPartial->next()); + $onlyFields[] = $nested; + } catch (ErrorException $exception) { + $this->handleNonExistingNestedField($exception, PartialType::Only, $nested, $dataClass, $transformationContext); + } continue; } @@ -214,8 +227,12 @@ protected function resolveIncludedFields( } if ($nested = $includedPartial->getNested()) { - $fields[$nested]->addIncludedResolvedPartial($includedPartial->next()); - $includedFields[] = $nested; + try { + $fields[$nested]->addIncludedResolvedPartial($includedPartial->next()); + $includedFields[] = $nested; + } catch (ErrorException $exception) { + $this->handleNonExistingNestedField($exception, PartialType::Include, $nested, $dataClass, $transformationContext); + } continue; } @@ -258,7 +275,11 @@ protected function resolveExcludedFields( } if ($nested = $excludePartial->getNested()) { - $fields[$nested]->addExcludedResolvedPartial($excludePartial->next()); + try { + $fields[$nested]->addExcludedResolvedPartial($excludePartial->next()); + } catch (ErrorException $exception) { + $this->handleNonExistingNestedField($exception, PartialType::Exclude, $nested, $dataClass, $transformationContext); + } continue; } @@ -270,4 +291,29 @@ protected function resolveExcludedFields( return $excludedFields; } + + + protected function handleNonExistingNestedField( + ErrorException $exception, + PartialType $partialType, + string $field, + DataClass $dataClass, + TransformationContext $transformationContext, + ): void { + if (str_starts_with($exception->getMessage(), 'Undefined array key: ')) { + throw $exception; + } + + if(config('data.ignore_invalid_partials')){ + return; + } + + throw CannotPerformPartialOnDataField::create( + $exception, + $partialType, + $field, + $dataClass, + $transformationContext + ); + } } diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 411cb8e8..ed148485 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -47,7 +47,6 @@ public static function createFromConfig( return new self( dataClass: $dataClass, validationType: ValidationType::from($config['validation_type']), - // TODO: maybe also add these to config, we should do the same for transormation so maybe some config cleanup is needed mapPropertyNames: true, withoutMagicalCreation: false, ignoredMagicalMethods: null, diff --git a/src/Support/Partials/Partial.php b/src/Support/Partials/Partial.php index 2ebf5f6f..97a56792 100644 --- a/src/Support/Partials/Partial.php +++ b/src/Support/Partials/Partial.php @@ -120,6 +120,15 @@ public function resolve(BaseData|BaseDataCollectable $data): ?ResolvedPartial return null; } + public function toArray(): array + { + return [ + 'segments' => $this->segments, + 'permanent' => $this->permanent, + 'condition' => $this->condition, + ]; + } + public function __toString(): string { return implode('.', $this->segments); diff --git a/src/Support/Partials/PartialType.php b/src/Support/Partials/PartialType.php index 9d9cf64f..5cdac3ee 100644 --- a/src/Support/Partials/PartialType.php +++ b/src/Support/Partials/PartialType.php @@ -21,6 +21,16 @@ public function getRequestParameterName(): string }; } + public function getVerb(): string + { + return match ($this) { + self::Include => 'include', + self::Exclude => 'exclude', + self::Only => 'only', + self::Except => 'except', + }; + } + /** * @return string[]|null */ diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php index e11300c9..24369647 100644 --- a/src/Support/Partials/PartialsCollection.php +++ b/src/Support/Partials/PartialsCollection.php @@ -9,4 +9,25 @@ */ class PartialsCollection extends SplObjectStorage { + public static function create(Partial ...$partials): self + { + $collection = new self(); + + foreach ($partials as $partial) { + $collection->attach($partial); + } + + return $collection; + } + + public function toArray(): array + { + $output = []; + + foreach ($this as $partial) { + $output[] = $partial->toArray(); + } + + return $output; + } } diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php index 6324cdb8..c0722496 100644 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ b/src/Support/Partials/ResolvedPartialsCollection.php @@ -34,7 +34,7 @@ public function toArray(): array public function __toString(): string { - $output = "- partials:".PHP_EOL; + $output = ''; foreach ($this as $partial) { $output .= " - {$partial}".PHP_EOL; diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index da735de8..cde37e62 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -224,18 +224,22 @@ public function __toString(): string } if ($this->includePartials !== null && $this->includePartials->count() > 0) { + $output .= "- include partials:".PHP_EOL; $output .= $this->includePartials; } if ($this->excludePartials !== null && $this->excludePartials->count() > 0) { + $output .= "- exclude partials:".PHP_EOL; $output .= $this->excludePartials; } if ($this->onlyPartials !== null && $this->onlyPartials->count() > 0) { + $output .= "- only partials:".PHP_EOL; $output .= $this->onlyPartials; } if ($this->exceptPartials !== null && $this->exceptPartials->count() > 0) { + $output .= "- except partials:".PHP_EOL; $output .= $this->exceptPartials; } diff --git a/tests/CreationFactoryTest.php b/tests/CreationFactoryTest.php new file mode 100644 index 00000000..64e37775 --- /dev/null +++ b/tests/CreationFactoryTest.php @@ -0,0 +1,150 @@ +withoutMagicalCreation()->from(['hash_id' => 1, 'name' => 'Taylor']) + )->toEqual(new $data(null, 'Taylor')); + + expect($data::from(['hash_id' => 1, 'name' => 'Taylor'])) + ->toEqual(new $data(1, 'Taylor')); +}); + +it('can create data ignoring certain magical methods', function () { + $data = new class ('', '') extends Data { + public function __construct( + public ?string $id, + public string $name, + ) { + } + + public static function fromArray(array $payload) + { + return new self( + id: $payload['hash_id'] ?? null, + name: $payload['name'], + ); + } + }; + + expect( + $data::factory()->ignoreMagicalMethod('fromArray')->from(['hash_id' => 1, 'name' => 'Taylor']) + )->toEqual(new $data(null, 'Taylor')); + + expect( + $data::factory()->from(['hash_id' => 1, 'name' => 'Taylor']), + )->toEqual(new $data(1, 'Taylor')); +}); + +it('can enable the validation of non request payloads', function () { + $dataClass = new class () extends Data { + #[In('Hello World')] + public string $string; + }; + + $payload = [ + 'string' => 'nowp', + ]; + + expect($dataClass::factory()->from($payload)) + ->toBeInstanceOf(Data::class) + ->string->toEqual('nowp'); + + expect(fn () => $dataClass::factory()->alwaysValidate()->from($payload)) + ->toThrow(ValidationException::class); +}); + +it('can disable the validation request payloads', function () { + $dataClass = new class () extends Data { + #[In('Hello World')] + public string $string; + }; + + $request = request()->merge([ + 'string' => 'nowp', + ]); + + expect(fn () => $dataClass::factory()->from($request)) + ->toThrow(ValidationException::class); + + expect($dataClass::factory()->disableValidation()->from($request)) + ->toBeInstanceOf(Data::class) + ->string->toEqual('nowp'); +}); + +it('can disable property mapping', function () { + $dataClass = new class () extends Data { + #[MapInputName('firstName')] + public string $first_name; + }; + + expect($dataClass::factory()->withoutPropertyNameMapping()->from(['firstName' => 'Taylor'])) + ->toBeInstanceOf(Data::class) + ->first_name->toBeEmpty(); +}); + +it('can add a new global cast', function () { + $data = SimpleData::factory()->withCast('string', new StringToUpperCast())->from([ + 'string' => 'Hello World', + ]); + + expect($data)->string->toEqual('HELLO WORLD'); +}); + +it('can add a collection of global casts', function () { + $castCollection = new GlobalCastsCollection([ + 'string' => new StringToUpperCast(), + 'int' => new MeaningOfLifeCast(), + ]); + + $dataClass = new class extends Data { + public string $string; + + public int $int; + }; + + $data = $dataClass::factory()->withCastCollection($castCollection)->from([ + 'string' => 'Hello World', + 'int' => '123', + ]); + + expect($data)->string->toEqual('HELLO WORLD'); + expect($data)->int->toEqual(42); +}); + +it('can collect using a factory', function (){ + $collection = SimpleData::factory()->withCast('string', new StringToUpperCast())->collect([ + ['string' => 'Hello World'], + ['string' => 'Hello You'], + ], Collection::class); + + expect($collection) + ->toHaveCount(2) + ->first()->string->toEqual('HELLO WORLD') + ->last()->string->toEqual('HELLO YOU'); +}); diff --git a/tests/Fakes/Casts/MeaningOfLifeCast.php b/tests/Fakes/Casts/MeaningOfLifeCast.php new file mode 100644 index 00000000..598cc87e --- /dev/null +++ b/tests/Fakes/Casts/MeaningOfLifeCast.php @@ -0,0 +1,16 @@ +toEqual(new DataWithMultipleArgumentCreationMethod('Rick Astley_42')); }); -it('can disable the use of magical methods', function () { - $data = new class ('', '') extends Data { - public function __construct( - public ?string $id, - public string $name, - ) { - } - - public static function fromArray(array $payload) - { - return new self( - id: $payload['hash_id'] ?? null, - name: $payload['name'], - ); - } - }; - - expect( - $data::factory()->withoutMagicalCreation()->from(['hash_id' => 1, 'name' => 'Taylor']) - )->toEqual(new $data(null, 'Taylor')); - - expect($data::from(['hash_id' => 1, 'name' => 'Taylor'])) - ->toEqual(new $data(1, 'Taylor')); -}); - -it('can create data ignoring certain magical methods', function () { - class DummyA extends Data - { - public function __construct( - public ?string $id, - public string $name, - ) { - } - - public static function fromArray(array $payload) - { - return new self( - id: $payload['hash_id'] ?? null, - name: $payload['name'], - ); - } - } - - expect( - app(DataFromSomethingResolver::class)->execute( - DummyA::class, - CreationContextFactory::createFromConfig(DummyA::class)->ignoreMagicalMethod('fromArray')->get(), - ['hash_id' => 1, 'name' => 'Taylor'] - ) - )->toEqual(new DummyA(null, 'Taylor')); - - expect( - app(DataFromSomethingResolver::class)->execute( - DummyA::class, - CreationContextFactory::createFromConfig(DummyA::class)->get(), - ['hash_id' => 1, 'name' => 'Taylor'] - ) - )->toEqual(new DummyA(1, 'Taylor')); -}); - it('can inject the creation context when using a magical method', function () { $dataClass = new class () extends Data { public function __construct( diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 76bd5e72..e23ca093 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -6,6 +6,10 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; +use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; +use Spatie\LaravelData\Support\Partials\PartialType; use Spatie\LaravelData\Tests\Fakes\CircData; use Spatie\LaravelData\Tests\Fakes\DefaultLazyData; use Spatie\LaravelData\Tests\Fakes\DummyDto; @@ -1048,153 +1052,191 @@ public function __construct( ]); }); +it('will check if partials are valid as request partials', function ( + ?array $lazyDataAllowedIncludes, + ?array $dataAllowedIncludes, + ?string $includes, + ?PartialsCollection $expectedPartials, + array $expectedResponse +) { + LazyData::setAllowedIncludes($lazyDataAllowedIncludes); + + $data = new class ( + Lazy::create(fn () => 'Hello'), + Lazy::create(fn () => LazyData::from('Hello')), + Lazy::create(fn () => LazyData::collect(['Hello', 'World'])), + ) extends Data { + public static ?array $allowedIncludes; + + public function __construct( + public Lazy|string $property, + public Lazy|LazyData $nested, + #[DataCollectionOf(LazyData::class)] + public Lazy|array $collection, + ) { + } + + public static function allowedRequestIncludes(): ?array + { + return static::$allowedIncludes; + } + }; + + $data::$allowedIncludes = $dataAllowedIncludes; + + $request = request(); + + if ($includes !== null) { + $request->merge([ + 'include' => $includes, + ]); + } + + $partials = app(RequestQueryStringPartialsResolver::class)->execute($data, $request, PartialType::Include); + + expect($partials?->toArray())->toEqual($expectedPartials?->toArray()); + expect($data->toResponse($request)->getData(assoc: true))->toEqual($expectedResponse); +})->with(function () { + yield 'disallowed property inclusion' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => [], + 'includes' => 'property', + 'expectedPartials' => PartialsCollection::create(), + 'expectedResponse' => [], + ]; + + yield 'allowed property inclusion' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => ['property'], + 'includes' => 'property', + 'expectedPartials' => PartialsCollection::create( + Partial::create('property'), + ), + 'expectedResponse' => [ + 'property' => 'Hello', + ], + ]; + + yield 'allowed data property inclusion without nesting' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => ['nested'], + 'includes' => 'nested.name', + 'expectedPartials' => PartialsCollection::create( + Partial::create('nested'), + ), + 'expectedResponse' => [ + 'nested' => [], + ], + ]; + + yield 'allowed data property inclusion with nesting' => [ + 'lazyDataAllowedIncludes' => ['name'], + 'dataAllowedIncludes' => ['nested'], + 'includes' => 'nested.name', + 'expectedPartials' => PartialsCollection::create( + Partial::create('nested.name'), + ), + 'expectedResponse' => [ + 'nested' => [ + 'name' => 'Hello', + ], + ], + ]; -// Todo: replace -//it('will correctly reduce a tree based upon allowed includes', function ( -// ?array $lazyDataAllowedIncludes, -// ?array $dataAllowedIncludes, -// ?string $requestedAllowedIncludes, -// TreeNode $expectedIncludes -//) { -// LazyData::setAllowedIncludes($lazyDataAllowedIncludes); -// -// $data = new class ( -// 'Hello', -// LazyData::from('Hello'), -// LazyData::collect(['Hello', 'World']) -// ) extends Data { -// public static ?array $allowedIncludes; -// -// public function __construct( -// public string $property, -// public LazyData $nested, -// #[DataCollectionOf(LazyData::class)] -// public array $collection, -// ) { -// } -// -// public static function allowedRequestIncludes(): ?array -// { -// return static::$allowedIncludes; -// } -// }; -// -// $data::$allowedIncludes = $dataAllowedIncludes; -// -// $request = request(); -// -// if ($requestedAllowedIncludes !== null) { -// $request->merge([ -// 'include' => $requestedAllowedIncludes, -// ]); -// } -// -// $trees = $this->resolver->execute($data, $request); -// -// expect($trees->lazyIncluded)->toEqual($expectedIncludes); -//})->with(function () { -// yield 'disallowed property inclusion' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => [], -// 'requestedIncludes' => 'property', -// 'expectedIncludes' => new ExcludedTreeNode(), -// ]; -// -// yield 'allowed property inclusion' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['property'], -// 'requestedIncludes' => 'property', -// 'expectedIncludes' => new PartialTreeNode([ -// 'property' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data property inclusion without nesting' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data property inclusion with nesting' => [ -// 'lazyDataAllowedIncludes' => ['name'], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed data collection property inclusion without nesting' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['collection'], -// 'requestedIncludes' => 'collection.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'collection' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'allowed data collection property inclusion with nesting' => [ -// 'lazyDataAllowedIncludes' => ['name'], -// 'dataAllowedIncludes' => ['collection'], -// 'requestedIncludes' => 'collection.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'collection' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.name', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new PartialTreeNode([ -// 'name' => new ExcludedTreeNode(), -// ]), -// ]), -// ]; -// -// yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.*', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new AllTreeNode(), -// ]), -// ]; -// -// yield 'disallowed all nested data property inclusion ' => [ -// 'lazyDataAllowedIncludes' => [], -// 'dataAllowedIncludes' => ['nested'], -// 'requestedIncludes' => 'nested.*', -// 'expectedIncludes' => new PartialTreeNode([ -// 'nested' => new ExcludedTreeNode(), -// ]), -// ]; -// -// yield 'multi property inclusion' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested', 'property'], -// 'requestedIncludes' => 'nested.*,property', -// 'expectedIncludes' => new PartialTreeNode([ -// 'property' => new ExcludedTreeNode(), -// 'nested' => new AllTreeNode(), -// ]), -// ]; -// -// yield 'without property inclusion' => [ -// 'lazyDataAllowedIncludes' => null, -// 'dataAllowedIncludes' => ['nested', 'property'], -// 'requestedIncludes' => null, -// 'expectedIncludes' => new DisabledTreeNode(), -// ]; -//}); + yield 'allowed data collection property inclusion without nesting' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => ['collection'], + 'includes' => 'collection.name', + 'expectedPartials' => PartialsCollection::create( + Partial::create('collection'), + ), + 'expectedResponse' => [ + 'collection' => [ + [], + [] + ], + ], + ]; + + yield 'allowed data collection property inclusion with nesting' => [ + 'lazyDataAllowedIncludes' => ['name'], + 'dataAllowedIncludes' => ['collection'], + 'includes' => 'collection.name', + 'expectedPartials' => PartialsCollection::create( + Partial::create('collection.name'), + ), + 'expectedResponse' => [ + 'collection' => [ + ['name' => 'Hello'], + ['name' => 'World'], + ], + ], + ]; + + yield 'allowed nested data property inclusion without defining allowed includes on nested' => [ + 'lazyDataAllowedIncludes' => null, + 'dataAllowedIncludes' => ['nested'], + 'includes' => 'nested.name', + 'expectedPartials' => PartialsCollection::create( + Partial::create('nested.name'), + ), + 'expectedResponse' => [ + 'nested' => [ + 'name' => 'Hello', + ], + ], + ]; + + yield 'allowed all nested data property inclusion without defining allowed includes on nested' => [ + 'lazyDataAllowedIncludes' => null, + 'dataAllowedIncludes' => ['nested'], + 'includes' => 'nested.*', + 'expectedPartials' => PartialsCollection::create( + Partial::create('nested.*'), + ), + 'expectedResponse' => [ + 'nested' => [ + 'name' => 'Hello', + ], + ], + ]; + + yield 'disallowed all nested data property inclusion ' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => ['nested'], + 'includes' => 'nested.*', + 'expectedPartials' => PartialsCollection::create( + Partial::create('nested'), + ), + 'expectedResponse' => [ + 'nested' => [], + ], + ]; + + yield 'multi property inclusion' => [ + 'lazyDataAllowedIncludes' => null, + 'dataAllowedIncludes' => ['nested', 'property'], + 'includes' => 'nested.*,property', + 'expectedPartials' => PartialsCollection::create( + Partial::create('nested.*'), + Partial::create('property'), + ), + 'expectedResponse' => [ + 'property' => 'Hello', + 'nested' => [ + 'name' => 'Hello', + ], + ], + ]; + + yield 'without property inclusion' => [ + 'lazyDataAllowedIncludes' => null, + 'dataAllowedIncludes' => ['nested', 'property'], + 'includes' => null, + 'expectedPartials' => null, + 'expectedResponse' => [], + ]; +}); it('can combine request and manual includes', function () { $dataclass = new class ( diff --git a/tests/RequestTest.php b/tests/RequestTest.php index e60ac866..5dc8be32 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -6,17 +6,13 @@ use Illuminate\Support\Facades\Route; use Illuminate\Testing\TestResponse; use Illuminate\Validation\ValidationException; - -use function Pest\Laravel\handleExceptions; -use function Pest\Laravel\postJson; - use Spatie\LaravelData\Attributes\WithoutValidation; - use Spatie\LaravelData\Data; - - use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithExplicitValidationRuleAttributeData; +use function Pest\Laravel\handleExceptions; +use function Pest\Laravel\postJson; + function performRequest(string $string): TestResponse { @@ -61,6 +57,23 @@ function performRequest(string $string): TestResponse ->assertJson(['string' => 'Hello']); }); +it('is possible to overwrite the status response code', function () { + Route::post('/example-route', function () { + return new class(request()->input('string')) extends SimpleData { + protected function calculateResponseStatus(Request $request): int + { + return 301; + } + }; + }); + + postJson('/example-route', [ + 'string' => 'Hello', + ]) + ->assertStatus(301) + ->assertJson(['string' => 'Hello']); +}); + it('can fail validation', function () { Route::post('/example-route', function (SimpleDataWithExplicitValidationRuleAttributeData $data) { return ['email' => $data->email]; diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index a74d735c..3bba1b62 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -4,6 +4,7 @@ use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\Hidden; use Spatie\LaravelData\Data; +use Spatie\LaravelData\Exceptions\CannotPerformPartialOnDataField; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Resolvers\VisibleDataFieldsResolver; @@ -20,6 +21,7 @@ use Spatie\LaravelData\Tests\Fakes\FakeModelData; use Spatie\LaravelData\Tests\Fakes\FakeNestedModelData; use Spatie\LaravelData\Tests\Fakes\Models\FakeNestedModel; +use Spatie\LaravelData\Tests\Fakes\SimpleData; function findVisibleFields( Data $data, @@ -231,6 +233,39 @@ public static function create(string $name): static expect($dataClass::create('Freek')->toArray()['name'])->toBeInstanceOf(Closure::class); }); +it('will fail gracefully when a nested field does not exist', function () { + $dataClass = new class () extends Data { + public Lazy|SimpleData $simple; + + public Lazy|string $string; + + public function __construct() + { + $this->simple = Lazy::create(fn () => new SimpleData('Hello')); + $this->string = Lazy::create(fn () => 'World'); + } + }; + + expect(fn () => findVisibleFields($dataClass, TransformationContextFactory::create()->include('certainly-not-simple.string')))->toThrow( + CannotPerformPartialOnDataField::class + ); + + expect(fn () => $dataClass->include('certainly-not-simple.string')->toArray())->toThrow( + CannotPerformPartialOnDataField::class + ); + + config()->set('data.ignore_invalid_partials', true); + + expect(findVisibleFields($dataClass, TransformationContextFactory::create()->include('certainly-not-simple.string', 'string'))) + ->toEqual([ + 'string' => null, + ]); + + expect($dataClass->include('certainly-not-simple.string', 'string')->toArray())->toEqual([ + 'string' => 'World', + ]); +}); + class VisibleFieldsSingleData extends Data { public function __construct( diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 61428765..bfcdfe18 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -13,6 +13,7 @@ use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; +use Spatie\LaravelData\Support\Creation\ValidationType; use function Pest\Laravel\mock; use function PHPUnit\Framework\assertFalse; @@ -2086,24 +2087,6 @@ public static function fromRequest(Request $request): self assertFalse(true, 'We should not end up here'); }); -it('can validate non-requests payloads', function () { - $dataClass = new class () extends Data { - #[In('Hello World')] - public string $string; - }; - - $data = $dataClass::from([ - 'string' => 'nowp', - ]); - - expect($data)->toBeInstanceOf(Data::class) - ->string->toEqual('nowp'); - - $data = $dataClass::factory()->alwaysValidate()->from([ - 'string' => 'nowp', - ]); -})->throws(ValidationException::class); - it('can the validation rules for a data object', function () { expect(MultiData::getValidationRules([]))->toEqual([ 'first' => ['required', 'string'], @@ -2342,7 +2325,7 @@ public static function rules(ValidationContext $context): array DataValidationAsserter::for($dataClass) ->assertOk(['success' => true, 'id' => 1]) ->assertErrors(['success' => true]); -})->skip('V4: The rule inferrers need to be rewritten/removed for this, we need to first add attribute rules and then decide require stuff'); +})->skip('V5: The rule inferrers need to be rewritten/removed for this, we need to first add attribute rules and then decide require stuff'); it('can validate an optional but nonexists attribute', function () { $dataClass = new class () extends Data { @@ -2355,3 +2338,19 @@ public static function rules(ValidationContext $context): array expect($dataClass::from(['property' => []])->toArray())->toBe(['property' => []]); expect($dataClass::validateAndCreate([])->toArray())->toBe([]); }); + +it('is possible to define the validation type for each data object globally using config', function (){ + $dataClass = new class () extends Data { + #[In('Hello World')] + public string $string; + }; + + expect($dataClass::from(['string' => 'Nowp'])) + ->toBeInstanceOf(Data::class) + ->string->toBe('Nowp'); + + config()->set('data.validation_type', ValidationType::Always->value); + + expect(fn() => $dataClass::from(['string' => 'Nowp'])) + ->toThrow(ValidationException::class); +}); From 9e2ea32beef238a30e9d29cd0d30f1029bb05d95 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 17 Jan 2024 13:57:21 +0000 Subject: [PATCH 079/124] Fix styling --- src/Resolvers/VisibleDataFieldsResolver.php | 2 +- tests/CreationFactoryTest.php | 4 ++-- tests/PartialsTest.php | 2 +- tests/RequestTest.php | 9 +++++---- tests/ValidationTest.php | 7 ++++--- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index 5f7579e9..3e21e5b4 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -304,7 +304,7 @@ protected function handleNonExistingNestedField( throw $exception; } - if(config('data.ignore_invalid_partials')){ + if(config('data.ignore_invalid_partials')) { return; } diff --git a/tests/CreationFactoryTest.php b/tests/CreationFactoryTest.php index 64e37775..2c4836ba 100644 --- a/tests/CreationFactoryTest.php +++ b/tests/CreationFactoryTest.php @@ -122,7 +122,7 @@ public static function fromArray(array $payload) 'int' => new MeaningOfLifeCast(), ]); - $dataClass = new class extends Data { + $dataClass = new class () extends Data { public string $string; public int $int; @@ -137,7 +137,7 @@ public static function fromArray(array $payload) expect($data)->int->toEqual(42); }); -it('can collect using a factory', function (){ +it('can collect using a factory', function () { $collection = SimpleData::factory()->withCast('string', new StringToUpperCast())->collect([ ['string' => 'Hello World'], ['string' => 'Hello You'], diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index e23ca093..1a432d17 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -1153,7 +1153,7 @@ public static function allowedRequestIncludes(): ?array 'expectedResponse' => [ 'collection' => [ [], - [] + [], ], ], ]; diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 5dc8be32..1a2437ba 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -6,13 +6,14 @@ use Illuminate\Support\Facades\Route; use Illuminate\Testing\TestResponse; use Illuminate\Validation\ValidationException; + +use function Pest\Laravel\handleExceptions; +use function Pest\Laravel\postJson; + use Spatie\LaravelData\Attributes\WithoutValidation; use Spatie\LaravelData\Data; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithExplicitValidationRuleAttributeData; -use function Pest\Laravel\handleExceptions; -use function Pest\Laravel\postJson; - function performRequest(string $string): TestResponse { @@ -59,7 +60,7 @@ function performRequest(string $string): TestResponse it('is possible to overwrite the status response code', function () { Route::post('/example-route', function () { - return new class(request()->input('string')) extends SimpleData { + return new class (request()->input('string')) extends SimpleData { protected function calculateResponseStatus(Request $request): int { return 301; diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index bfcdfe18..1b57fc0c 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -13,11 +13,11 @@ use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; -use Spatie\LaravelData\Support\Creation\ValidationType; use function Pest\Laravel\mock; use function PHPUnit\Framework\assertFalse; use Spatie\LaravelData\Attributes\DataCollectionOf; + use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Attributes\Validation\ArrayType; @@ -41,6 +41,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Optional; +use Spatie\LaravelData\Support\Creation\ValidationType; use Spatie\LaravelData\Support\Validation\References\FieldReference; use Spatie\LaravelData\Support\Validation\References\RouteParameterReference; use Spatie\LaravelData\Support\Validation\ValidationContext; @@ -2339,7 +2340,7 @@ public static function rules(ValidationContext $context): array expect($dataClass::validateAndCreate([])->toArray())->toBe([]); }); -it('is possible to define the validation type for each data object globally using config', function (){ +it('is possible to define the validation type for each data object globally using config', function () { $dataClass = new class () extends Data { #[In('Hello World')] public string $string; @@ -2351,6 +2352,6 @@ public static function rules(ValidationContext $context): array config()->set('data.validation_type', ValidationType::Always->value); - expect(fn() => $dataClass::from(['string' => 'Nowp'])) + expect(fn () => $dataClass::from(['string' => 'Nowp'])) ->toThrow(ValidationException::class); }); From a32f36449738e52b79fc17777b8453328b350211 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 17 Jan 2024 14:59:43 +0100 Subject: [PATCH 080/124] Fix CI --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4130cdf..22dd8648 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "phpunit/phpunit": "^10.0", "spatie/invade": "^1.0", "spatie/laravel-typescript-transformer": "^2.3", - "spatie/pest-plugin-snapshots": "^2.0", + "spatie/pest-plugin-snapshots": "^2.1", "spatie/test-time": "^1.2" }, "autoload" : { From 05a1098915ead814169559e8e7d49a9983aad9e3 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 10:47:17 +0100 Subject: [PATCH 081/124] Replace type system --- benchmarks/TestBench.php | 60 +++ composer.json | 2 +- phpstan-baseline.neon | 25 - src/Commands/DataStructuresCacheCommand.php | 5 +- src/DataPipes/CastPropertiesDataPipe.php | 2 +- src/DataPipes/DefaultValuesDataPipe.php | 2 +- src/Exceptions/CannotFindDataClass.php | 28 +- .../DataCollectableFromSomethingResolver.php | 24 +- src/Resolvers/DataFromArrayResolver.php | 2 +- src/Resolvers/DataFromSomethingResolver.php | 2 +- src/Resolvers/DataValidationRulesResolver.php | 2 +- src/Resolvers/EmptyDataResolver.php | 7 +- src/RuleInferrers/NullableRuleInferrer.php | 2 +- src/RuleInferrers/RequiredRuleInferrer.php | 2 +- src/Support/Casting/GlobalCastsCollection.php | 2 +- src/Support/DataClass.php | 159 ------- src/Support/DataConfig.php | 2 +- src/Support/DataContainer.php | 10 + src/Support/DataMethod.php | 87 +--- src/Support/DataParameter.php | 26 +- src/Support/DataProperty.php | 56 +-- src/Support/DataReturnType.php | 20 + src/Support/DataType.php | 65 ++- src/Support/Factories/DataClassFactory.php | 200 ++++++++ src/Support/Factories/DataMethodFactory.php | 98 ++++ .../Factories/DataParameterFactory.php | 34 ++ src/Support/Factories/DataPropertyFactory.php | 89 ++++ .../Factories/DataReturnTypeFactory.php | 53 +++ src/Support/Factories/DataTypeFactory.php | 434 +++++++++++------- .../DataTypeScriptTransformer.php | 2 +- src/Support/Types/CombinationType.php | 32 ++ src/Support/Types/IntersectionType.php | 6 +- src/Support/Types/MultiType.php | 66 --- src/Support/Types/NamedType.php | 75 +++ src/Support/Types/PartialType.php | 137 ------ src/Support/Types/SingleType.php | 58 --- .../Types/Storage/AcceptedTypesStorage.php | 74 +++ src/Support/Types/Type.php | 46 +- src/Support/Types/UndefinedType.php | 26 -- src/Support/Types/UnionType.php | 6 +- tests/Casts/DateTimeInterfaceCastTest.php | 30 +- tests/Casts/EnumCastTest.php | 9 +- tests/Factories/FakeDataStructureFactory.php | 92 ++++ .../RequiredRuleInferrerTest.php | 3 +- .../Support/Caching/CachedDataConfigTest.php | 3 +- tests/Support/DataClassTest.php | 11 +- tests/Support/DataMethodTest.php | 47 +- tests/Support/DataParameterTest.php | 34 +- tests/Support/DataPropertyTest.php | 4 +- tests/Support/DataReturnTypeTest.php | 119 +++++ tests/Support/DataTypeTest.php | 430 +++++++++++------ .../DateTimeInterfaceTransformerTest.php | 27 +- tests/Transformers/EnumTransformerTest.php | 3 +- 53 files changed, 1728 insertions(+), 1112 deletions(-) create mode 100644 benchmarks/TestBench.php create mode 100644 src/Support/DataReturnType.php create mode 100644 src/Support/Factories/DataClassFactory.php create mode 100644 src/Support/Factories/DataMethodFactory.php create mode 100644 src/Support/Factories/DataParameterFactory.php create mode 100644 src/Support/Factories/DataPropertyFactory.php create mode 100644 src/Support/Factories/DataReturnTypeFactory.php create mode 100644 src/Support/Types/CombinationType.php delete mode 100644 src/Support/Types/MultiType.php create mode 100644 src/Support/Types/NamedType.php delete mode 100644 src/Support/Types/PartialType.php delete mode 100644 src/Support/Types/SingleType.php create mode 100644 src/Support/Types/Storage/AcceptedTypesStorage.php delete mode 100644 src/Support/Types/UndefinedType.php create mode 100644 tests/Factories/FakeDataStructureFactory.php create mode 100644 tests/Support/DataReturnTypeTest.php diff --git a/benchmarks/TestBench.php b/benchmarks/TestBench.php new file mode 100644 index 00000000..866c7c99 --- /dev/null +++ b/benchmarks/TestBench.php @@ -0,0 +1,60 @@ +createApplication(); + } + + protected function getPackageProviders($app) + { + return [ + LaravelDataServiceProvider::class, + ]; + } + + #[Revs(5000), Iterations(5)] + public function benchUseStored() + { + for ($i = 0; $i < 100; $i++) { + $this->runStored(); + } + } + + protected function runStored(): array + { + return AcceptedTypesStorage::getAcceptedTypes(Collection::class); + } + + #[Revs(5000), Iterations(5)] + public function benchUseNative() + { + for ($i = 0; $i < 100; $i++) { + $this->runNative(); + } + } + + protected function runNative(): array + { + return ! class_exists(Collection::class) ? [] : array_unique([ + ...array_values(class_parents(Collection::class)), + ...array_values(class_implements(Collection::class)), + ]); + } +} diff --git a/composer.json b/composer.json index 22dd8648..b2d15106 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require" : { - "php": "^8.1", + "php": "^8.2", "illuminate/contracts": "^10.0", "phpdocumentor/type-resolver": "^1.5", "spatie/laravel-package-tools": "^1.9.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5c688b1e..435d719d 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -125,36 +125,11 @@ parameters: count: 1 path: src/Support/DataConfig.php - - - message: "#^Match expression does not handle remaining values\\: \\(class\\-string\\&literal\\-string\\)\\|\\(class\\-string\\&literal\\-string\\)$#" - count: 1 - path: src/Support/Factories/DataTypeFactory.php - - - - message: "#^Match expression does not handle remaining values\\: \\(class\\-string\\&literal\\-string\\)\\|\\(class\\-string\\&literal\\-string\\)$#" - count: 1 - path: src/Support/Factories/DataTypeFactory.php - - message: "#^Parameter \\#1 \\$storage of method SplObjectStorage\\\\:\\:removeAll\\(\\) expects SplObjectStorage\\, Spatie\\\\LaravelData\\\\Support\\\\Partials\\\\PartialsCollection given\\.$#" count: 1 path: src/Support/Transformation/DataContext.php - - - message: "#^Call to an undefined method ReflectionType\\:\\:getName\\(\\)\\.$#" - count: 2 - path: src/Support/Types/MultiType.php - - - - message: "#^Parameter \\#1 \\$type of static method Spatie\\\\LaravelData\\\\Support\\\\Types\\\\PartialType\\:\\:create\\(\\) expects ReflectionNamedType, ReflectionType given\\.$#" - count: 1 - path: src/Support/Types/MultiType.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: src/Support/Types/MultiType.php - - message: "#^Call to an undefined method DateTimeInterface\\:\\:setTimezone\\(\\)\\.$#" count: 1 diff --git a/src/Commands/DataStructuresCacheCommand.php b/src/Commands/DataStructuresCacheCommand.php index 565875b0..a5b228d2 100644 --- a/src/Commands/DataStructuresCacheCommand.php +++ b/src/Commands/DataStructuresCacheCommand.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Support\Caching\DataStructureCache; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataStructuresCacheCommand extends Command { @@ -18,7 +19,7 @@ class DataStructuresCacheCommand extends Command public function handle( DataStructureCache $dataStructureCache, - DataConfig $dataConfig + DataClassFactory $dataClassFactory, ): void { $this->components->info('Caching data structures...'); @@ -31,7 +32,7 @@ public function handle( $progressBar = $this->output->createProgressBar(count($dataClasses)); foreach ($dataClasses as $dataClassString) { - $dataClass = DataClass::create(new ReflectionClass($dataClassString)); + $dataClass = $dataClassFactory->build(new ReflectionClass($dataClassString)); $dataClass->prepareForCache(); diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 2cf6158d..36edd69a 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -85,6 +85,6 @@ protected function shouldBeCasted(DataProperty $property, mixed $value): bool return true; // Transform everything to data objects } - return $property->type->type->acceptsValue($value) === false; + return $property->type->acceptsValue($value) === false; } } diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index 680a50e4..b865068b 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -32,7 +32,7 @@ public function handle( return; } - if ($property->type->isNullable()) { + if ($property->type->isNullable) { $properties[$property->name] = null; return; diff --git a/src/Exceptions/CannotFindDataClass.php b/src/Exceptions/CannotFindDataClass.php index cbfd9156..3b38d235 100644 --- a/src/Exceptions/CannotFindDataClass.php +++ b/src/Exceptions/CannotFindDataClass.php @@ -3,26 +3,26 @@ namespace Spatie\LaravelData\Exceptions; use Exception; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionProperty; class CannotFindDataClass extends Exception { - public static function noDataReferenceFound(string $class, string $propertyName): self + public static function forTypeable(ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable): self { - return new self("Property `{$propertyName}` in `{$class}` is not a data object or collection"); - } + if (is_string($typeable)) { + return new self("Cannot find data class for type `{$typeable}`"); + } - public static function missingDataCollectionAnotation(string $class, string $propertyName): self - { - return new self("Data collection property `{$propertyName}` in `{$class}` is missing an annotation with the type of data it represents"); - } + $class = $typeable->getDeclaringClass()->getName(); - public static function wrongDataCollectionAnnotation(string $class, string $propertyName): self - { - return new self("Data collection property `{$propertyName}` in `{$class}` has an annotation that isn't a data object or is missing an annotation"); - } + $name = match (true) { + $typeable instanceof ReflectionMethod => "method `{$class}::{{$typeable->getName()}`", + $typeable instanceof ReflectionProperty => "property `{$class}::{{$typeable->getName()}`", + $typeable instanceof ReflectionParameter => "parameter `{$class}::{$typeable->getDeclaringFunction()->getName()}::{$typeable->getName()}`", + }; - public static function cannotReadReflectionParameterDocblock(string $class, string $parameter): self - { - return new self("Data collection reflection parameter `{$parameter}` in `{$class}::__constructor` has an annotation that isn't a data object or is missing an annotation"); + return new self("Cannot find data class for {$name}"); } } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 4a75a109..0152a0a2 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -20,13 +20,15 @@ use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; -use Spatie\LaravelData\Support\Types\PartialType; +use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; +use Spatie\LaravelData\Support\Factories\DataTypeFactory; class DataCollectableFromSomethingResolver { public function __construct( protected DataConfig $dataConfig, protected DataFromSomethingResolver $dataFromSomethingResolver, + protected DataReturnTypeFactory $dataReturnTypeFactory, ) { } @@ -37,8 +39,8 @@ public function execute( ?string $into = null, ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { $intoType = $into !== null - ? PartialType::createFromTypeString($into) - : PartialType::createFromValue($items); + ? $this->dataReturnTypeFactory->buildFromNamedType($into) + : $this->dataReturnTypeFactory->buildFromValue($items); $collectable = $this->createFromCustomCreationMethod($dataClass, $creationContext, $items, $into); @@ -46,21 +48,19 @@ public function execute( return $collectable; } - $intoDataTypeKind = $intoType->getDataTypeKind(); - $collectableMetaData = CollectableMetaData::fromOther($items); $normalizedItems = $this->normalizeItems($items, $dataClass, $creationContext); - return match ($intoDataTypeKind) { + return match ($intoType->kind) { DataTypeKind::Array => $this->normalizeToArray($normalizedItems), - DataTypeKind::Enumerable => new $intoType->name($this->normalizeToArray($normalizedItems)), - DataTypeKind::DataCollection => new $intoType->name($dataClass, $this->normalizeToArray($normalizedItems)), - DataTypeKind::DataPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), - DataTypeKind::DataCursorPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::Enumerable => new $intoType->type->name($this->normalizeToArray($normalizedItems)), + DataTypeKind::DataCollection => new $intoType->type->name($dataClass, $this->normalizeToArray($normalizedItems)), + DataTypeKind::DataPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::DataCursorPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), DataTypeKind::Paginator => $this->normalizeToPaginator($normalizedItems, $collectableMetaData), DataTypeKind::CursorPaginator => $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData), - default => CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name) + default => throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->type->name) }; } @@ -107,7 +107,7 @@ protected function createFromCustomCreationMethod( $payload = []; foreach ($method->parameters as $parameter) { - if ($parameter->isCreationContext) { + if ($parameter->type->type->isCreationContext()) { $payload[$parameter->name] = $creationContext; } else { $payload[$parameter->name] = $this->normalizeItems($items, $dataClass, $creationContext); diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index ac8f66d8..33eb7e7d 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -63,7 +63,7 @@ public function execute(string $class, Collection $properties): BaseData } if ($property->computed - && $property->type->isNullable() + && $property->type->isNullable && $properties->get($property->name) === null ) { return; // Nullable properties get assigned null by default diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 5317121b..e13eeb29 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -95,7 +95,7 @@ protected function createFromCustomCreationMethod( } foreach ($method->parameters as $index => $parameter) { - if ($parameter->isCreationContext) { + if ($parameter->type->type->isCreationContext()) { $payloads[$index] = $creationContext; } } diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index da378f79..76dbf57b 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -103,7 +103,7 @@ protected function resolveDataSpecificRules( DataRules $dataRules, ): void { $isOptionalAndEmpty = $dataProperty->type->isOptional && Arr::has($fullPayload, $propertyPath->get()) === false; - $isNullableAndEmpty = $dataProperty->type->isNullable() && Arr::get($fullPayload, $propertyPath->get()) === null; + $isNullableAndEmpty = $dataProperty->type->isNullable && Arr::get($fullPayload, $propertyPath->get()) === null; if ($isOptionalAndEmpty || $isNullableAndEmpty) { $this->resolveToplevelRules( diff --git a/src/Resolvers/EmptyDataResolver.php b/src/Resolvers/EmptyDataResolver.php index de9fc01a..3509d246 100644 --- a/src/Resolvers/EmptyDataResolver.php +++ b/src/Resolvers/EmptyDataResolver.php @@ -6,7 +6,7 @@ use Spatie\LaravelData\Exceptions\DataPropertyCanOnlyHaveOneType; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Types\MultiType; +use Spatie\LaravelData\Support\Types\CombinationType; use Traversable; class EmptyDataResolver @@ -37,11 +37,12 @@ public function execute(string $class, array $extra = []): array protected function getValueForProperty(DataProperty $property): mixed { $propertyType = $property->type; - if ($propertyType->isMixed()) { + + if ($propertyType->isMixed) { return null; } - if ($propertyType->type instanceof MultiType && $propertyType->type->acceptedTypesCount() > 1) { + if ($propertyType->type instanceof CombinationType && count($propertyType->type->types) > 1) { throw DataPropertyCanOnlyHaveOneType::create($property); } diff --git a/src/RuleInferrers/NullableRuleInferrer.php b/src/RuleInferrers/NullableRuleInferrer.php index 0eba9fd8..5946c51d 100644 --- a/src/RuleInferrers/NullableRuleInferrer.php +++ b/src/RuleInferrers/NullableRuleInferrer.php @@ -14,7 +14,7 @@ public function handle( PropertyRules $rules, ValidationContext $context, ): PropertyRules { - if ($property->type->isNullable() && ! $rules->hasType(Nullable::class)) { + if ($property->type->isNullable && ! $rules->hasType(Nullable::class)) { $rules->prepend(new Nullable()); } diff --git a/src/RuleInferrers/RequiredRuleInferrer.php b/src/RuleInferrers/RequiredRuleInferrer.php index dfd72bf9..19c4f8d7 100644 --- a/src/RuleInferrers/RequiredRuleInferrer.php +++ b/src/RuleInferrers/RequiredRuleInferrer.php @@ -27,7 +27,7 @@ public function handle( protected function shouldAddRule(DataProperty $property, PropertyRules $rules): bool { - if ($property->type->isNullable() || $property->type->isOptional) { + if ($property->type->isNullable || $property->type->isOptional) { return false; } diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Casting/GlobalCastsCollection.php index 65fc0374..50eabb70 100644 --- a/src/Support/Casting/GlobalCastsCollection.php +++ b/src/Support/Casting/GlobalCastsCollection.php @@ -34,7 +34,7 @@ public function merge(self $casts): self public function findCastForValue(DataProperty $property): ?Cast { - foreach ($property->type->type->getAcceptedTypes() as $acceptedType => $baseTypes) { + foreach ($property->type->getAcceptedTypes() as $acceptedType => $baseTypes) { foreach ([$acceptedType, ...$baseTypes] as $type) { if ($cast = $this->casts[$type] ?? null) { return $cast; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 9b4bf41f..5c1fba43 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -56,165 +56,6 @@ public function __construct( ) { } - public static function create(ReflectionClass $class): self - { - /** @var class-string $name */ - $name = $class->name; - - $attributes = static::resolveAttributes($class); - - $methods = collect($class->getMethods()); - - $constructor = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor()); - - $dataCollectablePropertyAnnotations = DataCollectableAnnotationReader::create()->getForClass($class); - - if ($constructor) { - $dataCollectablePropertyAnnotations = array_merge( - $dataCollectablePropertyAnnotations, - DataCollectableAnnotationReader::create()->getForMethod($constructor) - ); - } - - $properties = self::resolveProperties( - $class, - $constructor, - NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes), - $dataCollectablePropertyAnnotations, - ); - - $responsable = $class->implementsInterface(ResponsableData::class); - - $outputMappedProperties = new LazyDataStructureProperty( - fn () => $properties - ->map(fn (DataProperty $property) => $property->outputMappedName) - ->filter() - ->flip() - ->toArray() - ); - - return new self( - name: $class->name, - properties: $properties, - methods: self::resolveMethods($class), - constructorMethod: DataMethod::createConstructor($constructor, $properties), - isReadonly: method_exists($class, 'isReadOnly') && $class->isReadOnly(), - isAbstract: $class->isAbstract(), - appendable: $class->implementsInterface(AppendableData::class), - includeable: $class->implementsInterface(IncludeableData::class), - responsable: $responsable, - transformable: $class->implementsInterface(TransformableData::class), - validateable: $class->implementsInterface(ValidateableData::class), - wrappable: $class->implementsInterface(WrappableData::class), - emptyData: $class->implementsInterface(EmptyData::class), - attributes: $attributes, - dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, - allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, - allowedRequestExcludes: $responsable ? $name::allowedRequestExcludes() : null, - allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, - allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, - outputMappedProperties: $outputMappedProperties, - transformationFields: static::resolveTransformationFields($properties), - ); - } - - protected static function resolveAttributes( - ReflectionClass $class - ): Collection { - $attributes = collect($class->getAttributes()) - ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) - ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); - - $parent = $class->getParentClass(); - - if ($parent !== false) { - $attributes = $attributes->merge(static::resolveAttributes($parent)); - } - - return $attributes; - } - - protected static function resolveMethods( - ReflectionClass $reflectionClass, - ): Collection { - return collect($reflectionClass->getMethods()) - ->filter(fn (ReflectionMethod $method) => str_starts_with($method->name, 'from') || str_starts_with($method->name, 'collect')) - ->reject(fn (ReflectionMethod $method) => in_array($method->name, ['from', 'collect', 'collection'])) - ->mapWithKeys( - fn (ReflectionMethod $method) => [$method->name => DataMethod::create($method)], - ); - } - - protected static function resolveProperties( - ReflectionClass $class, - ?ReflectionMethod $constructorMethod, - array $mappers, - array $dataCollectablePropertyAnnotations, - ): Collection { - $defaultValues = self::resolveDefaultValues($class, $constructorMethod); - - return collect($class->getProperties(ReflectionProperty::IS_PUBLIC)) - ->reject(fn (ReflectionProperty $property) => $property->isStatic()) - ->values() - ->mapWithKeys(fn (ReflectionProperty $property) => [ - $property->name => DataProperty::create( - $property, - array_key_exists($property->getName(), $defaultValues), - $defaultValues[$property->getName()] ?? null, - $mappers['inputNameMapper'], - $mappers['outputNameMapper'], - $dataCollectablePropertyAnnotations[$property->getName()] ?? null, - ), - ]); - } - - protected static function resolveDefaultValues( - ReflectionClass $class, - ?ReflectionMethod $constructorMethod, - ): array { - if (! $constructorMethod) { - return $class->getDefaultProperties(); - } - - $values = collect($constructorMethod->getParameters()) - ->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) - ->mapWithKeys(fn (ReflectionParameter $parameter) => [ - $parameter->name => $parameter->getDefaultValue(), - ]) - ->toArray(); - - return array_merge( - $class->getDefaultProperties(), - $values - ); - } - - /** - * @param Collection $properties - * - * @return LazyDataStructureProperty> - */ - protected static function resolveTransformationFields( - Collection $properties, - ): LazyDataStructureProperty { - $closure = fn () => $properties - ->reject(fn (DataProperty $property): bool => $property->hidden) - ->map(function (DataProperty $property): null|bool { - if ( - $property->type->kind->isDataCollectable() - || $property->type->kind->isDataObject() - || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) - ) { - return true; - } - - return null; - }) - ->all(); - - return new LazyDataStructureProperty($closure); - } - public function prepareForCache(): void { if($this->outputMappedProperties instanceof LazyDataStructureProperty) { diff --git a/src/Support/DataConfig.php b/src/Support/DataConfig.php index f58e9de7..1693fbfb 100644 --- a/src/Support/DataConfig.php +++ b/src/Support/DataConfig.php @@ -59,7 +59,7 @@ public function __construct( public function getDataClass(string $class): DataClass { - return $this->dataClasses[$class] ??= DataClass::create(new ReflectionClass($class)); + return $this->dataClasses[$class] ??= DataContainer::get()->dataClassFactory()->build(new ReflectionClass($class)); } /** diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index 3003a376..c8167d32 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -7,6 +7,8 @@ use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; +use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataContainer { @@ -22,6 +24,8 @@ class DataContainer protected ?DataCollectableFromSomethingResolver $dataCollectableFromSomethingResolver = null; + protected ?DataClassFactory $dataClassFactory = null; + private function __construct() { } @@ -60,6 +64,11 @@ public function dataCollectableFromSomethingResolver(): DataCollectableFromSomet return $this->dataCollectableFromSomethingResolver ??= app(DataCollectableFromSomethingResolver::class); } + public function dataClassFactory(): DataClassFactory + { + return $this->dataClassFactory ??= app(DataClassFactory::class); + } + public function reset() { $this->transformedDataResolver = null; @@ -67,5 +76,6 @@ public function reset() $this->requestQueryStringPartialsResolver = null; $this->dataFromSomethingResolver = null; $this->dataCollectableFromSomethingResolver = null; + $this->dataClassFactory = null; } } diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index fac973ee..50318ff7 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -3,11 +3,8 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; -use ReflectionMethod; -use ReflectionParameter; use Spatie\LaravelData\Enums\CustomCreationMethodType; -use Spatie\LaravelData\Support\Types\Type; -use Spatie\LaravelData\Support\Types\UndefinedType; +use Spatie\LaravelData\Support\OldTypes\OldType; /** * @property Collection $parameters @@ -20,84 +17,10 @@ public function __construct( public readonly bool $isStatic, public readonly bool $isPublic, public readonly CustomCreationMethodType $customCreationMethodType, - public readonly Type $returnType, + public readonly ?DataReturnType $returnType, ) { } - public static function create(ReflectionMethod $method): self - { - $returnType = Type::forReflection( - $method->getReturnType(), - $method->class, - ); - - return new self( - $method->name, - collect($method->getParameters())->map( - fn (ReflectionParameter $parameter) => DataParameter::create($parameter, $method->class), - ), - $method->isStatic(), - $method->isPublic(), - self::resolveCustomCreationMethodType($method, $returnType), - $returnType - ); - } - - public static function createConstructor(?ReflectionMethod $method, Collection $properties): ?self - { - if ($method === null) { - return null; - } - - $parameters = collect($method->getParameters()) - ->map(function (ReflectionParameter $parameter) use ($method, $properties) { - if (! $parameter->isPromoted()) { - return DataParameter::create($parameter, $method->class); - } - - if ($properties->has($parameter->name)) { - return $properties->get($parameter->name); - } - - return null; - }) - ->filter() - ->values(); - - return new self( - '__construct', - $parameters, - false, - $method->isPublic(), - CustomCreationMethodType::None, - new UndefinedType(), - ); - } - - protected static function resolveCustomCreationMethodType( - ReflectionMethod $method, - ?Type $returnType, - ): CustomCreationMethodType { - if (! $method->isStatic() - || ! $method->isPublic() - || $method->name === 'from' - || $method->name === 'collect' - || $method->name === 'collection' - ) { - return CustomCreationMethodType::None; - } - - if (str_starts_with($method->name, 'from')) { - return CustomCreationMethodType::Object; - } - - if (str_starts_with($method->name, 'collect') && ! $returnType instanceof UndefinedType) { - return CustomCreationMethodType::Collection; - } - - return CustomCreationMethodType::None; - } - public function accepts(mixed ...$input): bool { /** @var Collection $parameters */ @@ -106,7 +29,7 @@ public function accepts(mixed ...$input): bool : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); $parameters = $parameters->reject( - fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->isCreationContext + fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->type->type->isCreationContext() ); if (count($input) > $parameters->count()) { @@ -126,7 +49,7 @@ public function accepts(mixed ...$input): bool if ( $parameter instanceof DataProperty - && ! $parameter->type->type->acceptsValue($input[$index]) + && ! $parameter->type->acceptsValue($input[$index]) ) { return false; } @@ -144,6 +67,6 @@ public function accepts(mixed ...$input): bool public function returns(string $type): bool { - return $this->returnType->acceptsType($type); + return $this->returnType?->acceptsType($type) ?? false; } } diff --git a/src/Support/DataParameter.php b/src/Support/DataParameter.php index 8643763d..b0034911 100644 --- a/src/Support/DataParameter.php +++ b/src/Support/DataParameter.php @@ -4,8 +4,8 @@ use ReflectionParameter; use Spatie\LaravelData\Support\Creation\CreationContext; -use Spatie\LaravelData\Support\Types\SingleType; -use Spatie\LaravelData\Support\Types\Type; +use Spatie\LaravelData\Support\OldTypes\SingleOldType; +use Spatie\LaravelData\Support\OldTypes\OldType; class DataParameter { @@ -14,27 +14,7 @@ public function __construct( public readonly bool $isPromoted, public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, - public readonly Type $type, - // TODO: would be better if we refactor this to type, together with Castable, Lazy, etc - public readonly bool $isCreationContext, + public readonly DataType $type, ) { } - - public static function create( - ReflectionParameter $parameter, - string $class, - ): self { - $hasDefaultValue = $parameter->isDefaultValueAvailable(); - - $type = Type::forReflection($parameter->getType(), $class); - - return new self( - $parameter->name, - $parameter->isPromoted(), - $hasDefaultValue, - $hasDefaultValue ? $parameter->getDefaultValue() : null, - $type, - $type instanceof SingleType && $type->type->name === CreationContext::class - ); - } } diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 1a8b35c8..661f284d 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -16,6 +16,7 @@ use Spatie\LaravelData\Resolvers\NameMappersResolver; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Factories\DataTypeFactory; +use Spatie\LaravelData\Support\Factories\OldDataTypeFactory; use Spatie\LaravelData\Transformers\Transformer; /** @@ -41,59 +42,4 @@ public function __construct( public readonly Collection $attributes, ) { } - - public static function create( - ReflectionProperty $property, - bool $hasDefaultValue = false, - mixed $defaultValue = null, - ?NameMapper $classInputNameMapper = null, - ?NameMapper $classOutputNameMapper = null, - ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, - ): self { - $attributes = collect($property->getAttributes()) - ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) - ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); - - $mappers = NameMappersResolver::create()->execute($attributes); - - $inputMappedName = match (true) { - $mappers['inputNameMapper'] !== null => $mappers['inputNameMapper']->map($property->name), - $classInputNameMapper !== null => $classInputNameMapper->map($property->name), - default => null, - }; - - $outputMappedName = match (true) { - $mappers['outputNameMapper'] !== null => $mappers['outputNameMapper']->map($property->name), - $classOutputNameMapper !== null => $classOutputNameMapper->map($property->name), - default => null, - }; - - $computed = $attributes->contains( - fn (object $attribute) => $attribute instanceof Computed - ); - - $hidden = $attributes->contains( - fn (object $attribute) => $attribute instanceof Hidden - ); - - return new self( - name: $property->name, - className: $property->class, - type: DataTypeFactory::create()->build($property, $classDefinedDataCollectableAnnotation), - validate: ! $attributes->contains( - fn (object $attribute) => $attribute instanceof WithoutValidation - ) && ! $computed, - computed: $computed, - hidden: $hidden, - isPromoted: $property->isPromoted(), - isReadonly: $property->isReadOnly(), - 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 || $attribute instanceof WithCastAndTransformer)?->get(), - inputMappedName: $inputMappedName, - outputMappedName: $outputMappedName, - attributes: $attributes, - ); - } } diff --git a/src/Support/DataReturnType.php b/src/Support/DataReturnType.php new file mode 100644 index 00000000..3ad50e1c --- /dev/null +++ b/src/Support/DataReturnType.php @@ -0,0 +1,20 @@ +type->acceptsType($type); + } +} diff --git a/src/Support/DataType.php b/src/Support/DataType.php index ab669c05..be41ef0e 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -2,37 +2,78 @@ namespace Spatie\LaravelData\Support; -use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\DataTypeKind; +use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Support\Types\Type; +/** + * @template T of Type + */ class DataType { /** - * @param Type $type - * @param string|null $lazyType - * @param bool $isOptional - * @param DataTypeKind $kind - * @param class-string|null $dataClass - * @param string|null $dataCollectableClass + * @param class-string|null $lazyType */ public function __construct( public readonly Type $type, - public readonly ?string $lazyType, public readonly bool $isOptional, + public readonly bool $isNullable, + public readonly bool $isMixed, + public readonly ?string $lazyType, + // @note for now we have a one data type per type rule + // Meaning a type can be a data object of some type, data collection of some type or something else + // If we want to support multiple types in the future all we need to do is replace calls to these + // properties and handle everything correctly public readonly DataTypeKind $kind, public readonly ?string $dataClass, public readonly ?string $dataCollectableClass, ) { + + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + return $this->type->findAcceptedTypeForBaseType($class); } - public function isNullable(): bool + public function acceptsType(string $type): bool { - return $this->type->isNullable; + if ($this->isMixed) { + return true; + } + + return $this->type->acceptsType($type); } - public function isMixed(): bool + public function getAcceptedTypes(): array { - return $this->type->isMixed; + if($this->isMixed) { + return []; + } + + return $this->type->getAcceptedTypes(); + } + + public function acceptsValue(mixed $value): bool + { + if ($this->isMixed) { + return true; + } + + if ($this->isNullable && $value === null) { + return true; + } + + $type = gettype($value); + + $type = match ($type) { + 'integer' => 'int', + 'boolean' => 'bool', + 'double' => 'float', + 'object' => $value::class, + default => $type, + }; + + return $this->type->acceptsType($type); } } diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php new file mode 100644 index 00000000..d233691f --- /dev/null +++ b/src/Support/Factories/DataClassFactory.php @@ -0,0 +1,200 @@ + $name */ + $name = $reflectionClass->name; + + $attributes = $this->resolveAttributes($reflectionClass); + + $methods = collect($reflectionClass->getMethods()); + + $constructorReflectionMethod = $methods->first(fn (ReflectionMethod $method) => $method->isConstructor()); + + $dataCollectablePropertyAnnotations = $this->dataCollectableAnnotationReader->getForClass($reflectionClass); + + if ($constructorReflectionMethod) { + $dataCollectablePropertyAnnotations = array_merge( + $dataCollectablePropertyAnnotations, + $this->dataCollectableAnnotationReader->getForMethod($constructorReflectionMethod) + ); + } + + $properties = $this->resolveProperties( + $reflectionClass, + $constructorReflectionMethod, + NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes), + $dataCollectablePropertyAnnotations, + ); + + $responsable = $reflectionClass->implementsInterface(ResponsableData::class); + + $outputMappedProperties = new LazyDataStructureProperty( + fn () => $properties + ->map(fn (DataProperty $property) => $property->outputMappedName) + ->filter() + ->flip() + ->toArray() + ); + + $constructor = $constructorReflectionMethod + ? $this->methodFactory->buildConstructor($constructorReflectionMethod, $reflectionClass, $properties) + : null; + + return new DataClass( + name: $reflectionClass->name, + properties: $properties, + methods: $this->resolveMethods($reflectionClass), + constructorMethod: $constructor, + isReadonly: method_exists($reflectionClass, 'isReadOnly') && $reflectionClass->isReadOnly(), + isAbstract: $reflectionClass->isAbstract(), + appendable: $reflectionClass->implementsInterface(AppendableData::class), + includeable: $reflectionClass->implementsInterface(IncludeableData::class), + responsable: $responsable, + transformable: $reflectionClass->implementsInterface(TransformableData::class), + validateable: $reflectionClass->implementsInterface(ValidateableData::class), + wrappable: $reflectionClass->implementsInterface(WrappableData::class), + emptyData: $reflectionClass->implementsInterface(EmptyData::class), + attributes: $attributes, + dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, + allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, + allowedRequestExcludes: $responsable ? $name::allowedRequestExcludes() : null, + allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, + allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, + outputMappedProperties: $outputMappedProperties, + transformationFields: static::resolveTransformationFields($properties), + ); + } + + protected function resolveAttributes( + ReflectionClass $reflectionClass + ): Collection { + $attributes = collect($reflectionClass->getAttributes()) + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + + $parent = $reflectionClass->getParentClass(); + + if ($parent !== false) { + $attributes = $attributes->merge(static::resolveAttributes($parent)); + } + + return $attributes; + } + + protected function resolveMethods( + ReflectionClass $reflectionClass, + ): Collection { + return collect($reflectionClass->getMethods()) + ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'from') || str_starts_with($reflectionMethod->name, 'collect')) + ->reject(fn (ReflectionMethod $reflectionMethod) => in_array($reflectionMethod->name, ['from', 'collect', 'collection'])) + ->mapWithKeys( + fn (ReflectionMethod $reflectionMethod) => [$reflectionMethod->name => $this->methodFactory->build($reflectionMethod, $reflectionClass)], + ); + } + + protected function resolveProperties( + ReflectionClass $reflectionClass, + ?ReflectionMethod $constructorReflectionMethod, + array $mappers, + array $dataCollectablePropertyAnnotations, + ): Collection { + $defaultValues = $this->resolveDefaultValues($reflectionClass, $constructorReflectionMethod); + + return collect($reflectionClass->getProperties(ReflectionProperty::IS_PUBLIC)) + ->reject(fn (ReflectionProperty $property) => $property->isStatic()) + ->values() + ->mapWithKeys(fn (ReflectionProperty $property) => [ + $property->name => $this->propertyFactory->build( + $property, + $reflectionClass, + array_key_exists($property->getName(), $defaultValues), + $defaultValues[$property->getName()] ?? null, + $mappers['inputNameMapper'], + $mappers['outputNameMapper'], + $dataCollectablePropertyAnnotations[$property->getName()] ?? null, + ), + ]); + } + + protected function resolveDefaultValues( + ReflectionClass $reflectionClass, + ?ReflectionMethod $constructorReflectionMethod, + ): array { + if (! $constructorReflectionMethod) { + return $reflectionClass->getDefaultProperties(); + } + + $values = collect($constructorReflectionMethod->getParameters()) + ->filter(fn (ReflectionParameter $parameter) => $parameter->isPromoted() && $parameter->isDefaultValueAvailable()) + ->mapWithKeys(fn (ReflectionParameter $parameter) => [ + $parameter->name => $parameter->getDefaultValue(), + ]) + ->toArray(); + + return array_merge( + $reflectionClass->getDefaultProperties(), + $values + ); + } + + /** + * @param Collection $properties + * + * @return LazyDataStructureProperty> + */ + protected function resolveTransformationFields( + Collection $properties, + ): LazyDataStructureProperty { + $closure = fn () => $properties + ->reject(fn (DataProperty $property): bool => $property->hidden) + ->map(function (DataProperty $property): null|bool { + if ( + $property->type->kind->isDataCollectable() + || $property->type->kind->isDataObject() + || ($property->type->kind === DataTypeKind::Default && $property->type->type->acceptsType('array')) + ) { + return true; + } + + return null; + }) + ->all(); + + return new LazyDataStructureProperty($closure); + } +} diff --git a/src/Support/Factories/DataMethodFactory.php b/src/Support/Factories/DataMethodFactory.php new file mode 100644 index 00000000..c1f38621 --- /dev/null +++ b/src/Support/Factories/DataMethodFactory.php @@ -0,0 +1,98 @@ +getReturnType() + ? $this->returnTypeFactory->build($reflectionMethod->getReturnType()) + : null; + + return new DataMethod( + name: $reflectionMethod->name, + parameters: collect($reflectionMethod->getParameters())->map( + fn (ReflectionParameter $parameter) => $this->parameterFactory->build($parameter, $reflectionClass), + ), + isStatic: $reflectionMethod->isStatic(), + isPublic: $reflectionMethod->isPublic(), + customCreationMethodType: $this->resolveCustomCreationMethodType($reflectionMethod, $returnType), + returnType: $returnType + ); + } + + public function buildConstructor( + ReflectionMethod $reflectionMethod, + ReflectionClass $reflectionClass, + Collection $properties + ): DataMethod { + $parameters = collect($reflectionMethod->getParameters()) + ->map(function (ReflectionParameter $parameter) use ($reflectionClass, $properties) { + if (! $parameter->isPromoted()) { + return $this->parameterFactory->build($parameter, $reflectionClass); + } + + if ($properties->has($parameter->name)) { + return $properties->get($parameter->name); + } + + return null; + }) + ->filter() + ->values(); + + return new DataMethod( + name: '__construct', + parameters: $parameters, + isStatic: false, + isPublic: $reflectionMethod->isPublic(), + customCreationMethodType: CustomCreationMethodType::None, + returnType: null, + ); + } + + protected function resolveCustomCreationMethodType( + ReflectionMethod $method, + ?DataReturnType $returnType, + ): CustomCreationMethodType { + if (! $method->isStatic() + || ! $method->isPublic() + || $method->name === 'from' + || $method->name === 'collect' + || $method->name === 'collection' + ) { + return CustomCreationMethodType::None; + } + + if (str_starts_with($method->name, 'from')) { + return CustomCreationMethodType::Object; + } + + if (str_starts_with($method->name, 'collect') && $returnType?->kind->isDataCollectable()) { + return CustomCreationMethodType::Collection; + } + + return CustomCreationMethodType::None; + } +} diff --git a/src/Support/Factories/DataParameterFactory.php b/src/Support/Factories/DataParameterFactory.php new file mode 100644 index 00000000..3c747c02 --- /dev/null +++ b/src/Support/Factories/DataParameterFactory.php @@ -0,0 +1,34 @@ +isDefaultValueAvailable(); + + return new DataParameter( + $reflectionParameter->name, + $reflectionParameter->isPromoted(), + $hasDefaultValue, + $hasDefaultValue ? $reflectionParameter->getDefaultValue() : null, + $this->typeFactory->build( + $reflectionParameter->getType(), + $reflectionClass, + $reflectionParameter, + ), + ); + } +} diff --git a/src/Support/Factories/DataPropertyFactory.php b/src/Support/Factories/DataPropertyFactory.php new file mode 100644 index 00000000..509a9d7f --- /dev/null +++ b/src/Support/Factories/DataPropertyFactory.php @@ -0,0 +1,89 @@ +getAttributes()) + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + + $mappers = NameMappersResolver::create()->execute($attributes); + + $inputMappedName = match (true) { + $mappers['inputNameMapper'] !== null => $mappers['inputNameMapper']->map($reflectionProperty->name), + $classInputNameMapper !== null => $classInputNameMapper->map($reflectionProperty->name), + default => null, + }; + + $outputMappedName = match (true) { + $mappers['outputNameMapper'] !== null => $mappers['outputNameMapper']->map($reflectionProperty->name), + $classOutputNameMapper !== null => $classOutputNameMapper->map($reflectionProperty->name), + default => null, + }; + + $computed = $attributes->contains( + fn (object $attribute) => $attribute instanceof Computed + ); + + $hidden = $attributes->contains( + fn (object $attribute) => $attribute instanceof Hidden + ); + + $validate = ! $attributes->contains( + fn (object $attribute) => $attribute instanceof WithoutValidation + ) && ! $computed; + + return new DataProperty( + name: $reflectionProperty->name, + className: $reflectionProperty->class, + type: $this->typeFactory->build( + $reflectionProperty->getType(), + $reflectionClass, + $reflectionProperty, + $attributes, + $classDefinedDataCollectableAnnotation + ), + validate: $validate, + computed: $computed, + hidden: $hidden, + isPromoted: $reflectionProperty->isPromoted(), + isReadonly: $reflectionProperty->isReadOnly(), + hasDefaultValue: $reflectionProperty->isPromoted() ? $hasDefaultValue : $reflectionProperty->hasDefaultValue(), + defaultValue: $reflectionProperty->isPromoted() ? $defaultValue : $reflectionProperty->getDefaultValue(), + cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(), + transformer: $attributes->first(fn (object $attribute) => $attribute instanceof WithTransformer || $attribute instanceof WithCastAndTransformer)?->get(), + inputMappedName: $inputMappedName, + outputMappedName: $outputMappedName, + attributes: $attributes, + ); + } +} diff --git a/src/Support/Factories/DataReturnTypeFactory.php b/src/Support/Factories/DataReturnTypeFactory.php new file mode 100644 index 00000000..91ca4929 --- /dev/null +++ b/src/Support/Factories/DataReturnTypeFactory.php @@ -0,0 +1,53 @@ + */ + public static array $store = []; + + public function build(ReflectionType $type): DataReturnType + { + if (! $type instanceof ReflectionNamedType) { + throw new TypeError('At the moment return types can only be of one type'); + } + + return $this->buildFromNamedType($type->getName()); + } + + public function buildFromNamedType(string $name): DataReturnType + { + if (array_key_exists($name, self::$store)) { + return self::$store[$name]; + } + + $builtIn = in_array($name, ['array', 'bool', 'float', 'int', 'string', 'mixed', 'null']); + + ['acceptedTypes' => $acceptedTypes, 'kind' => $kind] = AcceptedTypesStorage::getAcceptedTypesAndKind($name); + + return static::$store[$name] = new DataReturnType( + type: new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: null, + dataCollectableClass: null, + ), + kind: $kind, + ); + } + + public function buildFromValue(mixed $value): DataReturnType + { + return self::buildFromNamedType(get_debug_type($value)); + } +} diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index 680ef05a..4e53a374 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -2,189 +2,185 @@ namespace Spatie\LaravelData\Support\Factories; -use Illuminate\Support\Arr; +use Exception; +use Illuminate\Support\Collection; +use ReflectionClass; use ReflectionIntersectionType; +use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; +use ReflectionType; use ReflectionUnionType; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Exceptions\CannotFindDataClass; -use Spatie\LaravelData\Exceptions\InvalidDataType; +use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\Factories\Concerns\RequiresTypeInformation; +use Spatie\LaravelData\Support\Lazy\ClosureLazy; +use Spatie\LaravelData\Support\Lazy\ConditionalLazy; +use Spatie\LaravelData\Support\Lazy\DefaultLazy; +use Spatie\LaravelData\Support\Lazy\InertiaLazy; +use Spatie\LaravelData\Support\Lazy\RelationalLazy; use Spatie\LaravelData\Support\Types\IntersectionType; -use Spatie\LaravelData\Support\Types\PartialType; -use Spatie\LaravelData\Support\Types\SingleType; -use Spatie\LaravelData\Support\Types\UndefinedType; +use Spatie\LaravelData\Support\Types\NamedType; +use Spatie\LaravelData\Support\Types\Storage\AcceptedTypesStorage; +use Spatie\LaravelData\Support\Types\Type; use Spatie\LaravelData\Support\Types\UnionType; use TypeError; class DataTypeFactory { - public static function create(): self - { - return new self(); + public function __construct( + protected DataCollectableAnnotationReader $dataCollectableAnnotationReader, + ) { } public function build( - ReflectionParameter|ReflectionProperty $property, + ?ReflectionType $reflectionType, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes = null, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, ): DataType { - $type = $property->getType(); - - $class = match ($property::class) { - ReflectionParameter::class => $property->getDeclaringClass()?->name, - ReflectionProperty::class => $property->class, - }; - - return match (true) { - $type === null => $this->buildForEmptyType(), - $type instanceof ReflectionNamedType => $this->buildForNamedType( - $property, - $type, - $class, + $properties = match (true) { + $reflectionType === null => $this->inferPropertiesForNoneType(), + $reflectionType instanceof ReflectionNamedType => $this->inferPropertiesForSingleType( + $reflectionType, + $reflectionClass, + $typeable, + $attributes, $classDefinedDataCollectableAnnotation ), - $type instanceof ReflectionUnionType || $type instanceof ReflectionIntersectionType => $this->buildForMultiType( - $property, - $type, - $class, + $reflectionType instanceof ReflectionUnionType || $reflectionType instanceof ReflectionIntersectionType => $this->inferPropertiesForCombinationType( + $reflectionType, + $reflectionClass, + $typeable, + $attributes, $classDefinedDataCollectableAnnotation ), default => throw new TypeError('Invalid reflection type') }; + + return new DataType( + type: $properties['type'], + isOptional: $properties['isOptional'], + isNullable: $reflectionType?->allowsNull() ?? true, + isMixed: $properties['isMixed'], + lazyType: $properties['lazyType'], + kind: $properties['kind'], + dataClass: $properties['dataClass'], + dataCollectableClass: $properties['dataCollectableClass'], + ); } - protected function buildForEmptyType(): DataType + /** + * @return array{ + * type: NamedType, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + */ + protected function inferPropertiesForNoneType(): array { - return new DataType( - new UndefinedType(), - null, - false, - DataTypeKind::Default, - null, - null + $type = new NamedType( + name: 'mixed', + builtIn: true, + acceptedTypes: [], + kind: DataTypeKind::Default, + dataClass: null, + dataCollectableClass: null, ); + + return [ + 'type' => $type, + 'isMixed' => true, + 'isOptional' => false, + 'lazyType' => null, + 'kind' => DataTypeKind::Default, + 'dataClass' => null, + 'dataCollectableClass' => null, + ]; } - protected function buildForNamedType( - ReflectionParameter|ReflectionProperty $reflectionProperty, + /** + * @return array{ + * type: NamedType, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + * + */ + protected function inferPropertiesForSingleType( ReflectionNamedType $reflectionType, - ?string $class, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, - ): DataType { - $type = SingleType::create($reflectionType, $class); - - if ($type->type->isLazy()) { - throw InvalidDataType::onlyLazy($reflectionProperty); - } - - if ($type->type->isOptional()) { - throw InvalidDataType::onlyOptional($reflectionProperty); - } - - $kind = DataTypeKind::Default; - $dataClass = null; - $dataCollectableClass = null; - - if (! $type->isMixed) { - [ - 'kind' => $kind, - 'dataClass' => $dataClass, - 'dataCollectableClass' => $dataCollectableClass, - ] = $this->resolveDataSpecificProperties( - $reflectionProperty, - $type->type, + ): array { + return [ + ...$this->inferPropertiesForNamedType( + $reflectionType->getName(), + $reflectionType->isBuiltin(), + $reflectionClass, + $typeable, + $attributes, $classDefinedDataCollectableAnnotation - ); - } - - return new DataType( - type: $type, - lazyType: null, - isOptional: false, - kind: $kind, - dataClass: $dataClass, - dataCollectableClass: $dataCollectableClass - ); + ), + 'isOptional' => false, + 'lazyType' => null, + ]; } - protected function buildForMultiType( - ReflectionParameter|ReflectionProperty $reflectionProperty, - ReflectionUnionType|ReflectionIntersectionType $multiReflectionType, - ?string $class, + /** + * @return array{ + * type: NamedType, + * isMixed: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + */ + protected function inferPropertiesForNamedType( + string $name, + bool $builtIn, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, - ): DataType { - $type = match ($multiReflectionType::class) { - ReflectionUnionType::class => UnionType::create($multiReflectionType, $class), - ReflectionIntersectionType::class => IntersectionType::create($multiReflectionType, $class), - }; - - $isOptional = false; - $kind = DataTypeKind::Default; - $dataClass = null; - $dataCollectableClass = null; - $lazyType = null; - - - foreach ($type->types as $subType) { - if($subType->isLazy()) { - $lazyType = $subType->name; - } - - $isOptional = $isOptional || $subType->isOptional(); - - if (($subType->builtIn === false || $subType->name === 'array') - && $subType->isLazy() === false - && $subType->isOptional() === false - ) { - if ($kind !== DataTypeKind::Default) { - continue; - } - - [ - 'kind' => $kind, - 'dataClass' => $dataClass, - 'dataCollectableClass' => $dataCollectableClass, - ] = $this->resolveDataSpecificProperties( - $reflectionProperty, - $subType, - $classDefinedDataCollectableAnnotation - ); - } - } - - if ($kind->isDataObject() && $type->acceptedTypesCount() > 1) { - throw InvalidDataType::unionWithData($reflectionProperty); + ): array { + if ($name === 'self' || $name === 'static') { + $name = is_string($reflectionClass) ? $reflectionClass : $reflectionClass->getName(); } - if ($kind->isDataCollectable() && $type->acceptedTypesCount() > 1) { - throw InvalidDataType::unionWithDataCollection($reflectionProperty); - } + $isMixed = $name === 'mixed'; - return new DataType( - type: $type, - lazyType: $lazyType, - isOptional: $isOptional, - kind: $kind, - dataClass: $dataClass, - dataCollectableClass: $dataCollectableClass, - ); - } - - protected function resolveDataSpecificProperties( - ReflectionParameter|ReflectionProperty $reflectionProperty, - PartialType $partialType, - ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, - ): array { - $kind = $partialType->getDataTypeKind(); + ['acceptedTypes' => $acceptedTypes, 'kind' => $kind] = AcceptedTypesStorage::getAcceptedTypesAndKind($name); if ($kind === DataTypeKind::Default) { return [ - 'kind' => DataTypeKind::Default, + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: null, + dataCollectableClass: null, + ), + 'isMixed' => $isMixed, + 'kind' => $kind, 'dataClass' => null, 'dataCollectableClass' => null, ]; @@ -192,47 +188,177 @@ protected function resolveDataSpecificProperties( if ($kind === DataTypeKind::DataObject) { return [ - 'kind' => DataTypeKind::DataObject, - 'dataClass' => $partialType->name, + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: $name, + dataCollectableClass: null, + ), + 'isMixed' => $isMixed, + 'kind' => $kind, + 'dataClass' => $name, 'dataCollectableClass' => null, ]; } - $dataClass = null; - - $attributes = $reflectionProperty instanceof ReflectionProperty - ? $reflectionProperty->getAttributes(DataCollectionOf::class) - : []; + /** @var ?DataCollectionOf $dataCollectionOfAttribute */ + $dataCollectionOfAttribute = $attributes?->first( + fn (object $attribute) => $attribute instanceof DataCollectionOf + ); - if ($attribute = Arr::first($attributes)) { - $dataClass = $attribute->getArguments()[0]; - } + $dataClass = $dataCollectionOfAttribute?->class; $dataClass ??= $classDefinedDataCollectableAnnotation?->dataClass; - $dataClass ??= $reflectionProperty instanceof ReflectionProperty - ? DataCollectableAnnotationReader::create()->getForProperty($reflectionProperty)?->dataClass + $dataClass ??= $typeable instanceof ReflectionProperty + ? $this->dataCollectableAnnotationReader->getForProperty($typeable)?->dataClass : null; if ($dataClass !== null) { return [ + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: $kind, + dataClass: $dataClass, + dataCollectableClass: $name, + ), + 'isMixed' => $isMixed, 'kind' => $kind, 'dataClass' => $dataClass, - 'dataCollectableClass' => $partialType->name, + 'dataCollectableClass' => $name, ]; } if (in_array($kind, [DataTypeKind::Array, DataTypeKind::Paginator, DataTypeKind::CursorPaginator, DataTypeKind::Enumerable])) { return [ + 'type' => new NamedType( + name: $name, + builtIn: $builtIn, + acceptedTypes: $acceptedTypes, + kind: DataTypeKind::Default, + dataClass: null, + dataCollectableClass: null, + ), + 'isMixed' => $isMixed, 'kind' => DataTypeKind::Default, 'dataClass' => null, 'dataCollectableClass' => null, ]; } - throw CannotFindDataClass::missingDataCollectionAnotation( - $reflectionProperty instanceof ReflectionProperty ? $reflectionProperty->class : 'unknown', - $reflectionProperty->name - ); + throw CannotFindDataClass::forTypeable($typeable); + } + + /** + * @return array{ + * type: Type, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + * + */ + protected function inferPropertiesForCombinationType( + ReflectionUnionType|ReflectionIntersectionType $reflectionType, + ReflectionClass|string $reflectionClass, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + ): array { + $isMixed = false; + $isOptional = false; + $lazyType = null; + + $kind = null; + $dataClass = null; + $dataCollectableClass = null; + + $subTypes = []; + + foreach ($reflectionType->getTypes() as $reflectionSubType) { + if ($reflectionSubType::class === ReflectionUnionType::class || $reflectionSubType::class === ReflectionIntersectionType::class) { + $properties = $this->inferPropertiesForCombinationType( + $reflectionSubType, + $reflectionClass, + $typeable, + $attributes, + $classDefinedDataCollectableAnnotation + ); + + $isMixed = $isMixed || $properties['isMixed']; + $isOptional = $isOptional || $properties['isOptional']; + $lazyType = $lazyType ?? $properties['lazyType']; + + $kind ??= $properties['kind']; + $dataClass ??= $properties['dataClass']; + $dataCollectableClass ??= $properties['dataCollectableClass']; + + $subTypes[] = $properties['type']; + + continue; + } + + /** @var ReflectionNamedType $reflectionSubType */ + + $name = $reflectionSubType->getName(); + + if ($name === Optional::class) { + $isOptional = true; + + continue; + } + + if ($name === 'null') { + continue; + } + + if (in_array($name, [Lazy::class, DefaultLazy::class, ClosureLazy::class, ConditionalLazy::class, RelationalLazy::class, InertiaLazy::class])) { + $lazyType = $name; + + continue; + } + + $properties = $this->inferPropertiesForNamedType( + $reflectionSubType->getName(), + $reflectionSubType->isBuiltin(), + $reflectionClass, + $typeable, + $attributes, + $classDefinedDataCollectableAnnotation + ); + + $isMixed = $isMixed || $properties['isMixed']; + + $kind ??= $properties['kind']; + $dataClass ??= $properties['dataClass']; + $dataCollectableClass ??= $properties['dataCollectableClass']; + + $subTypes[] = $properties['type']; + } + + $type = match (true) { + count($subTypes) === 0 => throw new Exception('Invalid reflected type'), + count($subTypes) === 1 => $subTypes[0], + $reflectionType::class === ReflectionUnionType::class => new UnionType($subTypes), + $reflectionType::class === ReflectionIntersectionType::class => new IntersectionType($subTypes), + default => throw new Exception('Invalid reflected type'), + }; + + return [ + 'type' => $type, + 'isMixed' => $isMixed, + 'isOptional' => $isOptional, + 'lazyType' => $lazyType, + 'kind' => $kind, + 'dataClass' => $dataClass, + 'dataCollectableClass' => $dataCollectableClass, + ]; } } diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 504d14c1..80154255 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -114,7 +114,7 @@ protected function resolveTypeForProperty( default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') }; - if ($dataProperty->type->isNullable()) { + if ($dataProperty->type->isNullable) { return new Nullable($collectionType); } diff --git a/src/Support/Types/CombinationType.php b/src/Support/Types/CombinationType.php new file mode 100644 index 00000000..18bb95fb --- /dev/null +++ b/src/Support/Types/CombinationType.php @@ -0,0 +1,32 @@ + $types + */ + public function __construct( + public readonly array $types, + ) { + } + + public function getAcceptedTypes(): array + { + $types = []; + + foreach ($this->types as $type) { + foreach ($type->getAcceptedTypes() as $name => $acceptedTypes) { + $types[$name] = $acceptedTypes; + } + } + + return $types; + } + + public function isCreationContext(): bool + { + return false; + } +} diff --git a/src/Support/Types/IntersectionType.php b/src/Support/Types/IntersectionType.php index e60408d8..295bcc68 100644 --- a/src/Support/Types/IntersectionType.php +++ b/src/Support/Types/IntersectionType.php @@ -2,14 +2,10 @@ namespace Spatie\LaravelData\Support\Types; -class IntersectionType extends MultiType +class IntersectionType extends CombinationType { public function acceptsType(string $type): bool { - if ($this->isMixed) { - return true; - } - foreach ($this->types as $subType) { if (! $subType->acceptsType($type)) { return false; diff --git a/src/Support/Types/MultiType.php b/src/Support/Types/MultiType.php deleted file mode 100644 index 2d2e8cf5..00000000 --- a/src/Support/Types/MultiType.php +++ /dev/null @@ -1,66 +0,0 @@ -allowsNull(); - $isMixed = false; - $types = []; - - foreach ($multiType->getTypes() as $type) { - if ($type->getName() === 'null') { - continue; - } - - if ($type->getName() === 'mixed') { - $isMixed = true; - } - - $types[] = PartialType::create($type, $class); - } - - return new static( - $isNullable, - $isMixed, - $types - ); - } - - public function getAcceptedTypes(): array - { - $types = []; - - foreach ($this->types as $type) { - $types[$type->name] = $type->acceptedTypes; - } - - return $types; - } - - public function acceptedTypesCount(): int - { - return count(array_filter( - $this->types, - fn (PartialType $subType) => ! $subType->isLazy() && ! $subType->isOptional() - )); - } -} diff --git a/src/Support/Types/NamedType.php b/src/Support/Types/NamedType.php new file mode 100644 index 00000000..7666d187 --- /dev/null +++ b/src/Support/Types/NamedType.php @@ -0,0 +1,75 @@ + $acceptedTypes + * @param DataTypeKind $kind + * @param class-string|null $dataClass + * @param string|class-string|null $dataCollectableClass + */ + public function __construct( + public readonly string $name, + public readonly bool $builtIn, + public readonly array $acceptedTypes, + public readonly DataTypeKind $kind, + public readonly ?string $dataClass, + public readonly ?string $dataCollectableClass, + ) { + $this->isCastable = in_array(Castable::class, $this->acceptedTypes); + } + + public function acceptsType(string $type): bool + { + if ($type === $this->name) { + return true; + } + + if ($this->builtIn) { + return false; + } + + if (in_array($this->name, [$type, ...AcceptedTypesStorage::getAcceptedTypes($type)], true)) { + return true; + } + + return false; + } + + public function findAcceptedTypeForBaseType(string $class): ?string + { + if ($class === $this->name) { + return $class; + } + + if (in_array($class, $this->acceptedTypes)) { + return $this->name; + } + + return null; + } + + public function getAcceptedTypes(): array + { + return [ + $this->name => $this->acceptedTypes, + ]; + } + + public function isCreationContext(): bool + { + return $this->name === CreationContext::class; + } +} diff --git a/src/Support/Types/PartialType.php b/src/Support/Types/PartialType.php deleted file mode 100644 index c79a8938..00000000 --- a/src/Support/Types/PartialType.php +++ /dev/null @@ -1,137 +0,0 @@ -getName(); - - if ($typeName === 'mixed' || $type->isBuiltin()) { - return new self($typeName, true, []); - } - - if ($typeName === 'self' || $typeName === 'static') { - $typeName = $class; - } - - return new self( - name: $typeName, - builtIn: $type->isBuiltin(), - acceptedTypes: self::resolveAcceptedTypes($typeName) - ); - } - - public static function createFromTypeString(string $type): self - { - $builtIn = in_array($type, ['float', 'bool', 'int', 'array', 'string', 'mixed']); - - return new self( - name: $type, - builtIn: $builtIn, - acceptedTypes: ! $builtIn - ? self::resolveAcceptedTypes($type) - : [] - ); - } - - public static function createFromValue(mixed $value): self - { - return self::createFromTypeString(get_debug_type($value)); - } - - public function acceptsType(string $type): bool - { - if ($type === $this->name) { - return true; - } - - if ($this->builtIn) { - return false; - } - - // TODO: move this to some store for caching? - $baseTypes = class_exists($type) - ? array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]) - : []; - - if (in_array($this->name, [$type, ...$baseTypes], true)) { - return true; - } - - return false; - } - - public function findAcceptedTypeForBaseType(string $class): ?string - { - if ($class === $this->name) { - return $class; - } - - if (in_array($class, $this->acceptedTypes)) { - return $this->name; - } - - return null; - } - - public function isLazy(): bool - { - return $this->name === Lazy::class || in_array(Lazy::class, $this->acceptedTypes); - } - - public function isOptional(): bool - { - return $this->name === Optional::class || in_array(Optional::class, $this->acceptedTypes); - } - - public function getDataTypeKind(): DataTypeKind - { - return match (true) { - in_array(BaseData::class, $this->acceptedTypes) => DataTypeKind::DataObject, - $this->name === 'array' => DataTypeKind::Array, - in_array(Enumerable::class, $this->acceptedTypes) => DataTypeKind::Enumerable, - in_array(DataCollection::class, $this->acceptedTypes) || $this->name === DataCollection::class => DataTypeKind::DataCollection, - in_array(PaginatedDataCollection::class, $this->acceptedTypes) || $this->name === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, - in_array(CursorPaginatedDataCollection::class, $this->acceptedTypes) || $this->name === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, - in_array(Paginator::class, $this->acceptedTypes) || in_array(AbstractPaginator::class, $this->acceptedTypes) => DataTypeKind::Paginator, - in_array(CursorPaginator::class, $this->acceptedTypes) || in_array(AbstractCursorPaginator::class, $this->acceptedTypes) => DataTypeKind::CursorPaginator, - default => DataTypeKind::Default, - }; - } - - protected static function resolveAcceptedTypes(string $type): array - { - return array_unique([ - ...array_values(class_parents($type)), - ...array_values(class_implements($type)), - ]); - } -} diff --git a/src/Support/Types/SingleType.php b/src/Support/Types/SingleType.php deleted file mode 100644 index d81f19fd..00000000 --- a/src/Support/Types/SingleType.php +++ /dev/null @@ -1,58 +0,0 @@ -getName() === 'null') { - throw new Exception('Cannot create a single null type'); - } - - return new self( - isNullable: $reflectionType->allowsNull(), - isMixed: $reflectionType->getName() === 'mixed', - type: PartialType::create($reflectionType, $class) - ); - } - - public function acceptsType(string $type): bool - { - if ($this->isMixed) { - return true; - } - - return $this->type->acceptsType($type); - } - - public function findAcceptedTypeForBaseType(string $class): ?string - { - return $this->type->findAcceptedTypeForBaseType($class); - } - - public function getAcceptedTypes(): array - { - if ($this->isMixed) { - return []; - } - - return [ - $this->type->name => $this->type->acceptedTypes, - ]; - } -} diff --git a/src/Support/Types/Storage/AcceptedTypesStorage.php b/src/Support/Types/Storage/AcceptedTypesStorage.php new file mode 100644 index 00000000..4af27c92 --- /dev/null +++ b/src/Support/Types/Storage/AcceptedTypesStorage.php @@ -0,0 +1,74 @@ + */ + public static array $acceptedTypes = []; + + /** @var array */ + public static array $acceptedKinds = []; + + /** @return array{acceptedTypes:string[], kind: DataTypeKind} */ + public static function getAcceptedTypesAndKind(string $name): array + { + $acceptedTypes = static::getAcceptedTypes($name); + + return [ + 'acceptedTypes' => $acceptedTypes, + 'kind' => static::$acceptedKinds[$name] ??= static::resolveDataTypeKind($name, $acceptedTypes), + ]; + } + + /** @return string[] */ + public static function getAcceptedTypes(string $name): array + { + return static::$acceptedTypes[$name] ??= static::resolveAcceptedTypes($name); + } + + /** @return string[] */ + protected static function resolveAcceptedTypes(string $name): array + { + if (! class_exists($name)) { + return []; + } + + return array_unique([ + ...array_values(class_parents($name)), + ...array_values(class_implements($name)), + ]); + } + + protected static function resolveDataTypeKind(string $name, array $acceptedTypes): DataTypeKind + { + return match (true) { + in_array(BaseData::class, $acceptedTypes) => DataTypeKind::DataObject, + $name === 'array' => DataTypeKind::Array, + in_array(Enumerable::class, $acceptedTypes) => DataTypeKind::Enumerable, + in_array(DataCollection::class, $acceptedTypes) || $name === DataCollection::class => DataTypeKind::DataCollection, + in_array(PaginatedDataCollection::class, $acceptedTypes) || $name === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, + in_array(CursorPaginatedDataCollection::class, $acceptedTypes) || $name === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, + in_array(Paginator::class, $acceptedTypes) || in_array(AbstractPaginator::class, $acceptedTypes) => DataTypeKind::Paginator, + in_array(CursorPaginator::class, $acceptedTypes) || in_array(AbstractCursorPaginator::class, $acceptedTypes) => DataTypeKind::CursorPaginator, + default => DataTypeKind::Default, + }; + } + + public static function reset(): void + { + static::$acceptedTypes = []; + static::$acceptedKinds = []; + } +} diff --git a/src/Support/Types/Type.php b/src/Support/Types/Type.php index 9259b8da..3ec7d984 100644 --- a/src/Support/Types/Type.php +++ b/src/Support/Types/Type.php @@ -2,54 +2,16 @@ namespace Spatie\LaravelData\Support\Types; -use ReflectionIntersectionType; -use ReflectionNamedType; -use ReflectionType; -use ReflectionUnionType; - abstract class Type { - public function __construct( - public readonly bool $isNullable, - public readonly bool $isMixed, - ) { - } - - public static function forReflection( - ?ReflectionType $type, - string $class, - ): self { - return match (true) { - $type instanceof ReflectionNamedType => SingleType::create($type, $class), - $type instanceof ReflectionUnionType => UnionType::create($type, $class), - $type instanceof ReflectionIntersectionType => IntersectionType::create($type, $class), - default => new UndefinedType(), - }; - } - abstract public function acceptsType(string $type): bool; abstract public function findAcceptedTypeForBaseType(string $class): ?string; - // TODO: remove this? + /** + * @return array> + */ abstract public function getAcceptedTypes(): array; - public function acceptsValue(mixed $value): bool - { - if ($this->isNullable && $value === null) { - return true; - } - - $type = gettype($value); - - $type = match ($type) { - 'integer' => 'int', - 'boolean' => 'bool', - 'double' => 'float', - 'object' => $value::class, - default => $type, - }; - - return $this->acceptsType($type); - } + abstract public function isCreationContext(): bool; } diff --git a/src/Support/Types/UndefinedType.php b/src/Support/Types/UndefinedType.php deleted file mode 100644 index 3792fe9f..00000000 --- a/src/Support/Types/UndefinedType.php +++ /dev/null @@ -1,26 +0,0 @@ -isMixed) { - return true; - } - foreach ($this->types as $subType) { if ($subType->acceptsType($type)) { return true; diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 239abea9..5354f224 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -9,6 +9,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; it('can cast date times', function () { $caster = new DateTimeInterfaceCast('d-m-Y H:i:s'); @@ -23,9 +24,10 @@ public DateTimeImmutable $dateTimeImmutable; }; + expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -34,7 +36,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -43,7 +45,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -52,7 +54,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -69,7 +71,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -86,7 +88,7 @@ expect( $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'int')), + FakeDataStructureFactory::property($class, 'int'), '1994-05-16 12:20:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -108,7 +110,7 @@ }; expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -117,7 +119,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -126,7 +128,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -135,7 +137,7 @@ ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -158,7 +160,7 @@ }; expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -167,7 +169,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -176,7 +178,7 @@ ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -185,7 +187,7 @@ ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); expect($caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', collect(), CreationContextFactory::createFromConfig($class::class)->get() diff --git a/tests/Casts/EnumCastTest.php b/tests/Casts/EnumCastTest.php index a9ded990..6b285874 100644 --- a/tests/Casts/EnumCastTest.php +++ b/tests/Casts/EnumCastTest.php @@ -4,6 +4,7 @@ use Spatie\LaravelData\Casts\Uncastable; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\Enums\DummyUnitEnum; @@ -18,7 +19,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), 'foo', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -33,7 +34,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), 'bar', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -48,7 +49,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), 'foo', collect(), CreationContextFactory::createFromConfig($class::class)->get() @@ -63,7 +64,7 @@ expect( $this->caster->cast( - DataProperty::create(new ReflectionProperty($class, 'int')), + FakeDataStructureFactory::property($class, 'int'), 'foo', collect(), CreationContextFactory::createFromConfig($class::class)->get(), diff --git a/tests/Factories/FakeDataStructureFactory.php b/tests/Factories/FakeDataStructureFactory.php new file mode 100644 index 00000000..82c325cf --- /dev/null +++ b/tests/Factories/FakeDataStructureFactory.php @@ -0,0 +1,92 @@ +build($class); + } + + public static function method( + ReflectionMethod $method, + ): DataMethod + { + $factory = static::$methodFactory ??= app(DataMethodFactory::class); + + return $factory->build($method, $method->getDeclaringClass()); + } + + public static function constructor( + ReflectionMethod $method, + Collection $properties + ): DataMethod + { + $factory = static::$methodFactory ??= app(DataMethodFactory::class); + + return $factory->buildConstructor($method, $method->getDeclaringClass(), $properties); + } + + public static function property( + object $class, + string $name, + ): DataProperty { + $reflectionClass = new ReflectionClass($class); + $reflectionProperty = new ReflectionProperty($class, $name); + + $factory = static::$propertyFactory ??= app(DataPropertyFactory::class); + + return $factory->build($reflectionProperty, $reflectionClass); + } + + public static function parameter( + ReflectionParameter $parameter, + ): DataParameter { + $factory = static::$parameterFactory ??= app(DataParameterFactory::class); + + return $factory->build($parameter, $parameter->getDeclaringClass()); + } + + public static function returnType( + ReflectionMethod $method, + ): ?DataType { + $factory = static::$returnTypeFactory ??= app(DataReturnTypeFactory::class); + + return $factory->build($method->getReturnType()); + } +} diff --git a/tests/RuleInferrers/RequiredRuleInferrerTest.php b/tests/RuleInferrers/RequiredRuleInferrerTest.php index c6dfd078..654a7127 100644 --- a/tests/RuleInferrers/RequiredRuleInferrerTest.php +++ b/tests/RuleInferrers/RequiredRuleInferrerTest.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\Support\Validation\RuleDenormalizer; use Spatie\LaravelData\Support\Validation\ValidationContext; use Spatie\LaravelData\Support\Validation\ValidationPath; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; /** @@ -24,7 +25,7 @@ */ function getProperty(object $class) { - $dataClass = DataClass::create(new ReflectionClass($class)); + $dataClass = FakeDataStructureFactory::class($class); return $dataClass->properties->first(); } diff --git a/tests/Support/Caching/CachedDataConfigTest.php b/tests/Support/Caching/CachedDataConfigTest.php index 87f5b2b6..885acb73 100644 --- a/tests/Support/Caching/CachedDataConfigTest.php +++ b/tests/Support/Caching/CachedDataConfigTest.php @@ -6,6 +6,7 @@ use Spatie\LaravelData\Support\Caching\DataStructureCache; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; it('will use a cached data config if available', function () { @@ -41,7 +42,7 @@ }); it('will load cached data classes', function () { - $dataClass = DataClass::create(new ReflectionClass(SimpleData::class)); + $dataClass = FakeDataStructureFactory::class(SimpleData::class); $dataClass->prepareForCache(); $mock = Mockery::mock( diff --git a/tests/Support/DataClassTest.php b/tests/Support/DataClassTest.php index 61724a0c..458af352 100644 --- a/tests/Support/DataClassTest.php +++ b/tests/Support/DataClassTest.php @@ -8,13 +8,14 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataMethod; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMapper; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; it('keeps track of a global map from attribute', function () { - $dataClass = DataClass::create(new ReflectionClass(DataWithMapper::class)); + $dataClass = FakeDataStructureFactory::class(DataWithMapper::class); expect($dataClass->properties->get('casedProperty')->inputMappedName) ->toEqual('cased_property') @@ -23,7 +24,7 @@ }); it('will provide information about special methods', function () { - $class = DataClass::create(new ReflectionClass(SimpleData::class)); + $class = FakeDataStructureFactory::class(SimpleData::class); expect($class->methods)->toHaveKey('fromString') ->and($class->methods->get('fromString')) @@ -31,7 +32,7 @@ }); it('will provide information about the constructor', function () { - $class = DataClass::create(new ReflectionClass(SimpleData::class)); + $class = FakeDataStructureFactory::class(SimpleData::class); expect($class->constructorMethod) ->not->toBeNull() @@ -52,7 +53,7 @@ public function __construct( }; /** @var \Spatie\LaravelData\Support\DataProperty[] $properties */ - $properties = DataClass::create(new ReflectionClass($dataClass::class))->properties->values(); + $properties = FakeDataStructureFactory::class($dataClass::class)->properties->values(); expect($properties[0]) ->name->toEqual('property') @@ -97,7 +98,7 @@ public function __construct( } } - $dataClass = DataClass::create(new ReflectionClass(TestRecursiveAttributesChildData::class)); + $dataClass = FakeDataStructureFactory::class(TestRecursiveAttributesChildData::class); expect($dataClass->attributes) ->toHaveCount(3) diff --git a/tests/Support/DataMethodTest.php b/tests/Support/DataMethodTest.php index 23772d39..22b9e80a 100644 --- a/tests/Support/DataMethodTest.php +++ b/tests/Support/DataMethodTest.php @@ -5,9 +5,9 @@ use Illuminate\Support\Enumerable; use Spatie\LaravelData\Data; use Spatie\LaravelData\Enums\CustomCreationMethodType; -use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMultipleArgumentCreationMethod; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -21,9 +21,9 @@ public function __construct( } }; - $method = DataMethod::createConstructor( + $method = FakeDataStructureFactory::constructor( new ReflectionMethod($class, '__construct'), - collect(['promotedProperty' => DataProperty::create(new ReflectionProperty($class, 'promotedProperty'))]) + collect(['promotedProperty' => FakeDataStructureFactory::property($class, 'promotedProperty')]), ); expect($method) @@ -44,7 +44,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->name->toEqual('fromString') @@ -63,7 +63,7 @@ public static function collectArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectArray')); expect($method) ->name->toEqual('collectArray') @@ -74,9 +74,7 @@ public static function collectArray( ->and($method->parameters[0])->toBeInstanceOf(DataParameter::class); expect($method->returnType) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toBe(['array' => []]); + ->type->getAcceptedTypes()->toBe(['array' => []]); }); it('can create a data method from a magic collect method with nullable return type', function () { @@ -87,15 +85,13 @@ public static function collectArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectArray')); expect($method) ->customCreationMethodType->toBe(CustomCreationMethodType::Collection); expect($method->returnType) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toBe(['array' => []]); + ->type->getAcceptedTypes()->toBe(['array' => []]); }); it('will not create a magical collection method when no return type specified', function () { @@ -106,15 +102,12 @@ public static function collectArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectArray')); expect($method) ->customCreationMethodType->toBe(CustomCreationMethodType::None); - expect($method->returnType) - ->isNullable->toBeTrue() - ->isMixed->toBeTrue() - ->getAcceptedTypes()->toBe([]); + expect($method->returnType)->toBeNull(); }); it('correctly accepts single values as magic creation method', function () { @@ -125,7 +118,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts('Hello')->toBeTrue() @@ -140,13 +133,13 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method->accepts(new SimpleData('Hello')))->toBeTrue(); }); it('correctly accepts multiple values as magic creation method', function () { - $method = DataMethod::create(new ReflectionMethod(DataWithMultipleArgumentCreationMethod::class, 'fromMultiple')); + $method = FakeDataStructureFactory::method(new ReflectionMethod(DataWithMultipleArgumentCreationMethod::class, 'fromMultiple')); expect($method) ->accepts('Hello', 42)->toBeTrue() @@ -165,7 +158,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts(new SimpleData('Hello'))->toBeTrue() @@ -180,7 +173,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts('Hello')->toBeTrue() @@ -196,7 +189,7 @@ public static function fromString( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'fromString')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'fromString')); expect($method) ->accepts('Hello')->toBeTrue() @@ -213,7 +206,7 @@ public static function collectCollection( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollection')); expect($method->returns(Collection::class))->toBeTrue(); }); @@ -226,7 +219,7 @@ public static function collectCollection( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollection')); expect($method->returns(EloquentCollection::class))->toBeTrue(); }); @@ -239,7 +232,7 @@ public static function collectCollectionToArray( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollectionToArray')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollectionToArray')); expect($method->returns('array'))->toBeTrue(); }); @@ -253,7 +246,7 @@ public static function collectCollection( } }; - $method = DataMethod::create(new ReflectionMethod($class, 'collectCollection')); + $method = FakeDataStructureFactory::method(new ReflectionMethod($class, 'collectCollection')); expect($method->returns(Enumerable::class))->toBeFalse(); }); diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index bd23f78d..6c3da41b 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -4,7 +4,9 @@ use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataParameter; -use Spatie\LaravelData\Support\Types\Type; +use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\OldTypes\OldType; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; it('can create a data parameter', function () { @@ -20,57 +22,57 @@ public function __construct( }; $reflection = new ReflectionParameter([$class::class, '__construct'], 'nonPromoted'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('nonPromoted') ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'withoutType'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('withoutType') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'property'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('property') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'creationContext'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('creationContext') ->isPromoted->toBeFalse() ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeTrue(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeTrue(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'propertyWithDefault'); - $parameter = DataParameter::create($reflection, $class::class); + $parameter = FakeDataStructureFactory::parameter($reflection); expect($parameter) ->name->toEqual('propertyWithDefault') ->isPromoted->toBeTrue() ->hasDefaultValue->toBeTrue() ->defaultValue->toEqual('hello') - ->type->toEqual(Type::forReflection($reflection->getType(), $class::class)) - ->isCreationContext->toBeFalse(); + ->type->toBeInstanceOf(DataType::class) + ->type->type->isCreationContext()->toBeFalse(); }); diff --git a/tests/Support/DataPropertyTest.php b/tests/Support/DataPropertyTest.php index 54f096fb..0485a466 100644 --- a/tests/Support/DataPropertyTest.php +++ b/tests/Support/DataPropertyTest.php @@ -13,6 +13,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Factories\DataPropertyFactory; use Spatie\LaravelData\Tests\Fakes\CastTransformers\FakeCastTransformer; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -24,8 +25,9 @@ function resolveHelper( mixed $defaultValue = null ): DataProperty { $reflectionProperty = new ReflectionProperty($class, 'property'); + $reflectionClass = new ReflectionClass($class); - return DataProperty::create($reflectionProperty, $hasDefaultValue, $defaultValue); + return app(DataPropertyFactory::class)->build($reflectionProperty, $reflectionClass, $hasDefaultValue, $defaultValue); } it('can get the cast attribute with arguments', function () { diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php new file mode 100644 index 00000000..c68b5ca4 --- /dev/null +++ b/tests/Support/DataReturnTypeTest.php @@ -0,0 +1,119 @@ +getReturnType(); + + expect($factory->build($reflection))->toEqual($expected); + + expect($factory->buildFromNamedType($typeName))->toEqual($expected); + + expect($factory->buildFromValue($value))->toEqual($expected); +})->with(function (){ + yield 'array' => [ + 'methodName' => 'array', + 'typeName' => 'array', + 'value' => [], + new DataReturnType( + type: new NamedType('array', true, [], DataTypeKind::Array, null, null), + kind: DataTypeKind::Array, + ), + ]; + + yield 'collection' => [ + 'methodName' => 'collection', + 'typeName' => Collection::class, + 'value' => collect(), + new DataReturnType( + type: new NamedType(Collection::class, false, [ + ArrayAccess::class, + CanBeEscapedWhenCastToString::class, + Enumerable::class, + Traversable::class, + Stringable::class, + JsonSerializable::class, + Jsonable::class, + IteratorAggregate::class, + Countable::class, + Arrayable::class, + ], DataTypeKind::Enumerable, null, null), + kind: DataTypeKind::Enumerable, + ), + ]; + + yield 'data collection' => [ + 'methodName' => 'dataCollection', + 'typeName' => DataCollection::class, + 'value' => new DataCollection(SimpleData::class, []), + new DataReturnType( + type: new NamedType(DataCollection::class, false, [ + DataCollectable::class, + ArrayAccess::class, + Traversable::class, + ContextableData::class, + Castable::class, + Arrayable::class, + Jsonable::class, + JsonSerializable::class, + Countable::class, + IteratorAggregate::class, + WrappableData::class, + IncludeableData::class, + TransformableData::class, + ResponsableData::class, + BaseDataCollectable::class, + Responsable::class + + ], DataTypeKind::DataCollection, null, null), + kind: DataTypeKind::DataCollection, + ), + ]; +}); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index 0d22cab0..09e09831 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -27,12 +27,15 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Support\Lazy\RelationalLazy; +use Spatie\LaravelData\Support\Types\IntersectionType; +use Spatie\LaravelData\Support\Types\NamedType; +use Spatie\LaravelData\Support\Types\UnionType; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -40,7 +43,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType { - $class = DataClass::create(new ReflectionClass($class)); + $class = FakeDataStructureFactory::class($class); return $class->properties->get($property)->type; } @@ -52,15 +55,21 @@ function resolveDataType(object $class, string $property = 'property'): DataType expect($type) ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeTrue() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->lazyType->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toBe([]); expect($type->type) - ->isMixed->toBeTrue() - ->isNullable->toBeTrue() - ->getAcceptedTypes()->toBe([]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('mixed') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a type with definition', function () { @@ -69,16 +78,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); expect($type->type) - ->isMixed->toBeFalse() - ->isNullable->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a nullable type with definition', function () { @@ -87,16 +102,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectionClass->toBeNull(); + ->dataCollectionClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); expect($type->type) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a union type definition', function () { @@ -105,16 +126,17 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string', 'int']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string', 'int']); + ->toBeInstanceOf(UnionType::class); }); it('can deduce a nullable union type definition', function () { @@ -123,16 +145,17 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string', 'int']); expect($type->type) - ->isNullable->toBeTrue() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string', 'int']); + ->toBeInstanceOf(UnionType::class); }); it('can deduce an intersection type definition', function () { @@ -141,19 +164,42 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys([ + DateTime::class, + DateTimeImmutable::class, + ]); expect($type->type) - ->isNullable->toBeFalse() + ->toBeInstanceOf(IntersectionType::class); +}); + +it('can deduce a nullable intersection type definition', function () { + $type = resolveDataType(new class () { + public (DateTime & DateTimeImmutable)|null $property; + }); + + expect($type) + ->isOptional->toBeFalse() + ->isNullable->toBeTrue() ->isMixed->toBeFalse() + ->lazyType->toBeNull() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull() ->getAcceptedTypes()->toHaveKeys([ DateTime::class, DateTimeImmutable::class, ]); + + expect($type->type) + ->toBeInstanceOf(IntersectionType::class); }); it('can deduce a mixed type', function () { @@ -162,16 +208,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeTrue() + ->isMixed->toBeTrue() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toBeEmpty(); expect($type->type) - ->isNullable->toBeTrue() - ->isMixed->toBeTrue() - ->getAcceptedTypes()->toHaveKeys([]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('mixed') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce a lazy type', function () { @@ -180,16 +232,23 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); + expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); it('can deduce an optional type', function () { @@ -198,40 +257,46 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeTrue() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Default) ->dataClass->toBeNull() - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys(['string']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['string']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('string') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Default) + ->dataClass->toBeNull() + ->dataCollectableClass->toBeNull(); }); -test('a type cannot be optional alone', function () { - resolveDataType(new class () { - public Optional $property; - }); -})->throws(InvalidDataType::class); - it('can deduce a data type', function () { $type = resolveDataType(new class () { public SimpleData $property; }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataObject) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(SimpleData::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataObject) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBeNull(); }); it('can deduce a data union type', function () { @@ -240,16 +305,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataObject) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBeNull(); + ->dataCollectableClass->toBeNull() + ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([SimpleData::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(SimpleData::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataObject) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBeNull(); }); it('can deduce a data collection type', function () { @@ -259,16 +330,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class); + ->dataCollectableClass->toBe(DataCollection::class) + ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(DataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); }); it('can deduce a data collection union type', function () { @@ -278,16 +355,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(DataCollection::class); + ->dataCollectableClass->toBe(DataCollection::class) + ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([DataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(DataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(DataCollection::class); }); it('can deduce a paginated data collection type', function () { @@ -297,16 +380,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(PaginatedDataCollection::class); + ->dataCollectableClass->toBe(PaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(PaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(PaginatedDataCollection::class); }); it('can deduce a paginated data collection union type', function () { @@ -316,16 +405,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(PaginatedDataCollection::class); + ->dataCollectableClass->toBe(PaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([PaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(PaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(PaginatedDataCollection::class); }); it('can deduce a cursor paginated data collection type', function () { @@ -335,16 +430,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); }); it('can deduce a cursor paginated data collection union type', function () { @@ -354,16 +455,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginatedDataCollection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginatedDataCollection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::DataCursorPaginatedCollection) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginatedDataCollection::class); }); it('can deduce an array data collection type', function () { @@ -373,16 +480,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe('array'); + ->dataCollectableClass->toBe('array') + ->getAcceptedTypes()->toHaveKeys(['array']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['array']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('array') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array'); }); it('can deduce an array data collection union type', function () { @@ -392,16 +505,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Array) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe('array'); + ->dataCollectableClass->toBe('array') + ->getAcceptedTypes()->toHaveKeys(['array']); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys(['array']); + ->toBeInstanceOf(NamedType::class) + ->name->toBe('array') + ->builtIn->toBeTrue() + ->kind->toBe(DataTypeKind::Array) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe('array'); }); it('can deduce an enumerable data collection type', function () { @@ -411,16 +530,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(Collection::class); + ->dataCollectableClass->toBe(Collection::class) + ->getAcceptedTypes()->toHaveKeys([Collection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([Collection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(Collection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Enumerable) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(Collection::class); }); it('can deduce an enumerable data collection union type', function () { @@ -430,16 +555,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Enumerable) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(Collection::class); + ->dataCollectableClass->toBe(Collection::class) + ->getAcceptedTypes()->toHaveKeys([Collection::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([Collection::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(Collection::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Enumerable) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(Collection::class); }); it('can deduce a paginator data collection type', function () { @@ -449,16 +580,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(LengthAwarePaginator::class); + ->dataCollectableClass->toBe(LengthAwarePaginator::class) + ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(LengthAwarePaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Paginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(LengthAwarePaginator::class); }); it('can deduce a paginator data collection union type', function () { @@ -468,16 +605,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::Paginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(LengthAwarePaginator::class); + ->dataCollectableClass->toBe(LengthAwarePaginator::class) + ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(LengthAwarePaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::Paginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(LengthAwarePaginator::class); }); it('can deduce a cursor paginator data collection type', function () { @@ -487,16 +630,22 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBeNull() ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBeNull() ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginator::class); + ->dataCollectableClass->toBe(CursorPaginator::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::CursorPaginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginator::class); }); it('can deduce a cursor paginator data collection union type', function () { @@ -506,41 +655,47 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); expect($type) - ->lazyType->toBe(Lazy::class) ->isOptional->toBeFalse() + ->isNullable->toBeFalse() + ->isMixed->toBeFalse() + ->lazyType->toBe(Lazy::class) ->kind->toBe(DataTypeKind::CursorPaginator) ->dataClass->toBe(SimpleData::class) - ->dataCollectableClass->toBe(CursorPaginator::class); + ->dataCollectableClass->toBe(CursorPaginator::class) + ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); expect($type->type) - ->isNullable->toBeFalse() - ->isMixed->toBeFalse() - ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); + ->toBeInstanceOf(NamedType::class) + ->name->toBe(CursorPaginator::class) + ->builtIn->toBeFalse() + ->kind->toBe(DataTypeKind::CursorPaginator) + ->dataClass->toBe(SimpleData::class) + ->dataCollectableClass->toBe(CursorPaginator::class); }); it('cannot have multiple data types', function () { resolveDataType(new class () { public SimpleData|ComplicatedData $property; }); -})->throws(InvalidDataType::class); +})->skip('Do we want to always check this?')->throws(InvalidDataType::class); it('cannot combine a data object and another type', function () { resolveDataType(new class () { public SimpleData|int $property; }); -})->throws(InvalidDataType::class); +})->skip('Do we want to always check this?')->throws(InvalidDataType::class); it('cannot combine a data collection and another type', function () { resolveDataType(new class () { #[DataCollectionOf(SimpleData::class)] public DataCollection|int $property; }); -})->throws(InvalidDataType::class); +})->skip('Do we want to always check this?')->throws(InvalidDataType::class); it( 'will resolve the base types for accepted types', function (object $class, array $expected) { - expect(resolveDataType($class)->type->getAcceptedTypes())->toEqualCanonicalizing($expected); + expect(resolveDataType($class)->getAcceptedTypes())->toEqualCanonicalizing($expected); } )->with(function () { yield 'no type' => [ @@ -619,7 +774,7 @@ function (object $class, array $expected) { it( 'can check if a data type accepts a type', function (object $class, string $type, bool $accepts) { - expect(resolveDataType($class))->type->acceptsType($type)->toEqual($accepts); + expect(resolveDataType($class))->acceptsType($type)->toEqual($accepts); } )->with(function () { // Base types @@ -772,7 +927,7 @@ function (object $class, string $type, bool $accepts) { it( 'can check if a data type accepts a value', function (object $class, mixed $value, bool $accepts) { - expect(resolveDataType($class))->type->acceptsValue($value)->toEqual($accepts); + expect(resolveDataType($class))->acceptsValue($value)->toEqual($accepts); } )->with(function () { yield [ @@ -844,7 +999,6 @@ function (object $class, mixed $value, bool $accepts) { 'can find accepted type for a base type', function (object $class, string $type, ?string $expectedType) { expect(resolveDataType($class)) - ->type ->findAcceptedTypeForBaseType($type) ->toEqual($expectedType); } diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index 865bbbb2..cc260c11 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -5,6 +5,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; it('can transform dates', function () { @@ -22,7 +23,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), new Carbon('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -30,7 +31,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), new CarbonImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -38,7 +39,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), new DateTime('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -46,7 +47,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), new DateTimeImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -68,7 +69,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), new Carbon('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -76,7 +77,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), new CarbonImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -84,7 +85,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), new DateTime('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -92,7 +93,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), new DateTimeImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -114,7 +115,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), new Carbon('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -122,7 +123,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + FakeDataStructureFactory::property($class, 'carbonImmutable'), new CarbonImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -130,7 +131,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), + FakeDataStructureFactory::property($class, 'dateTime'), new DateTime('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -138,7 +139,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + FakeDataStructureFactory::property($class, 'dateTimeImmutable'), new DateTimeImmutable('19-05-1994 00:00:00'), TransformationContextFactory::create()->get($class) ) @@ -156,7 +157,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'carbon')), + FakeDataStructureFactory::property($class, 'carbon'), Carbon::createFromFormat('!Y-m-d', '1994-05-19'), TransformationContextFactory::create()->get($class) ) diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php index 32784cd0..8046c277 100644 --- a/tests/Transformers/EnumTransformerTest.php +++ b/tests/Transformers/EnumTransformerTest.php @@ -3,6 +3,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; +use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Transformers\EnumTransformer; @@ -15,7 +16,7 @@ expect( $transformer->transform( - DataProperty::create(new ReflectionProperty($class, 'enum')), + FakeDataStructureFactory::property($class, 'enum'), $class->enum, TransformationContextFactory::create()->get($class) ) From c5fd0585eb72781d892b86c5abcda29d51686236 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 19 Jan 2024 09:47:43 +0000 Subject: [PATCH 082/124] Fix styling --- src/Commands/DataStructuresCacheCommand.php | 2 -- .../DataCollectableFromSomethingResolver.php | 1 - src/Support/DataClass.php | 17 ----------------- src/Support/DataContainer.php | 1 - src/Support/DataMethod.php | 1 - src/Support/DataParameter.php | 5 ----- src/Support/DataProperty.php | 13 ------------- src/Support/Factories/DataMethodFactory.php | 3 --- src/Support/Factories/DataPropertyFactory.php | 4 ++-- src/Support/Factories/DataTypeFactory.php | 3 +-- tests/Casts/DateTimeInterfaceCastTest.php | 1 - tests/Casts/EnumCastTest.php | 1 - tests/Factories/FakeDataStructureFactory.php | 6 ++---- .../RuleInferrers/RequiredRuleInferrerTest.php | 1 - tests/Support/DataClassTest.php | 1 - tests/Support/DataParameterTest.php | 2 -- tests/Support/DataReturnTypeTest.php | 7 ++----- .../DateTimeInterfaceTransformerTest.php | 1 - tests/Transformers/EnumTransformerTest.php | 1 - 19 files changed, 7 insertions(+), 64 deletions(-) diff --git a/src/Commands/DataStructuresCacheCommand.php b/src/Commands/DataStructuresCacheCommand.php index a5b228d2..5d7ba039 100644 --- a/src/Commands/DataStructuresCacheCommand.php +++ b/src/Commands/DataStructuresCacheCommand.php @@ -7,8 +7,6 @@ use Spatie\LaravelData\Support\Caching\CachedDataConfig; use Spatie\LaravelData\Support\Caching\DataClassFinder; use Spatie\LaravelData\Support\Caching\DataStructureCache; -use Spatie\LaravelData\Support\DataClass; -use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataStructuresCacheCommand extends Command diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 0152a0a2..d430d273 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -21,7 +21,6 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; -use Spatie\LaravelData\Support\Factories\DataTypeFactory; class DataCollectableFromSomethingResolver { diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 5c1fba43..2c7194c5 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -3,24 +3,7 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; -use ReflectionAttribute; -use ReflectionClass; -use ReflectionMethod; -use ReflectionParameter; -use ReflectionProperty; -use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\DataObject; -use Spatie\LaravelData\Contracts\EmptyData; -use Spatie\LaravelData\Contracts\IncludeableData; -use Spatie\LaravelData\Contracts\ResponsableData; -use Spatie\LaravelData\Contracts\TransformableData; -use Spatie\LaravelData\Contracts\ValidateableData; -use Spatie\LaravelData\Contracts\WrappableData; -use Spatie\LaravelData\Data; -use Spatie\LaravelData\Enums\DataTypeKind; -use Spatie\LaravelData\Mappers\ProvidedNameMapper; -use Spatie\LaravelData\Resolvers\NameMappersResolver; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; /** * @property class-string $name diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index c8167d32..3f27d3e2 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -7,7 +7,6 @@ use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataContainer diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 50318ff7..52586f3c 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -4,7 +4,6 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Enums\CustomCreationMethodType; -use Spatie\LaravelData\Support\OldTypes\OldType; /** * @property Collection $parameters diff --git a/src/Support/DataParameter.php b/src/Support/DataParameter.php index b0034911..f4b8410d 100644 --- a/src/Support/DataParameter.php +++ b/src/Support/DataParameter.php @@ -2,11 +2,6 @@ namespace Spatie\LaravelData\Support; -use ReflectionParameter; -use Spatie\LaravelData\Support\Creation\CreationContext; -use Spatie\LaravelData\Support\OldTypes\SingleOldType; -use Spatie\LaravelData\Support\OldTypes\OldType; - class DataParameter { public function __construct( diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 661f284d..fa6e440c 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -3,20 +3,7 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; -use ReflectionAttribute; -use ReflectionProperty; -use Spatie\LaravelData\Attributes\Computed; -use Spatie\LaravelData\Attributes\GetsCast; -use Spatie\LaravelData\Attributes\Hidden; -use Spatie\LaravelData\Attributes\WithCastAndTransformer; -use Spatie\LaravelData\Attributes\WithoutValidation; -use Spatie\LaravelData\Attributes\WithTransformer; use Spatie\LaravelData\Casts\Cast; -use Spatie\LaravelData\Mappers\NameMapper; -use Spatie\LaravelData\Resolvers\NameMappersResolver; -use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; -use Spatie\LaravelData\Support\Factories\DataTypeFactory; -use Spatie\LaravelData\Support\Factories\OldDataTypeFactory; use Spatie\LaravelData\Transformers\Transformer; /** diff --git a/src/Support/Factories/DataMethodFactory.php b/src/Support/Factories/DataMethodFactory.php index c1f38621..1a9d20a4 100644 --- a/src/Support/Factories/DataMethodFactory.php +++ b/src/Support/Factories/DataMethodFactory.php @@ -8,10 +8,7 @@ use ReflectionParameter; use Spatie\LaravelData\Enums\CustomCreationMethodType; use Spatie\LaravelData\Support\DataMethod; -use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataReturnType; -use Spatie\LaravelData\Support\DataType; -use Spatie\LaravelData\Support\OldTypes\UndefinedOldType; class DataMethodFactory { diff --git a/src/Support/Factories/DataPropertyFactory.php b/src/Support/Factories/DataPropertyFactory.php index 509a9d7f..9a12c622 100644 --- a/src/Support/Factories/DataPropertyFactory.php +++ b/src/Support/Factories/DataPropertyFactory.php @@ -59,8 +59,8 @@ public function build( ); $validate = ! $attributes->contains( - fn (object $attribute) => $attribute instanceof WithoutValidation - ) && ! $computed; + fn (object $attribute) => $attribute instanceof WithoutValidation + ) && ! $computed; return new DataProperty( name: $reflectionProperty->name, diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index 4e53a374..c862a3cb 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -20,7 +20,6 @@ use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataType; -use Spatie\LaravelData\Support\Factories\Concerns\RequiresTypeInformation; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\DefaultLazy; @@ -305,7 +304,7 @@ protected function inferPropertiesForCombinationType( continue; } - /** @var ReflectionNamedType $reflectionSubType */ + /** @var ReflectionNamedType $reflectionSubType */ $name = $reflectionSubType->getName(); diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 5354f224..d7508618 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -8,7 +8,6 @@ use Spatie\LaravelData\Casts\Uncastable; use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContextFactory; -use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; it('can cast date times', function () { diff --git a/tests/Casts/EnumCastTest.php b/tests/Casts/EnumCastTest.php index 6b285874..791ba8d3 100644 --- a/tests/Casts/EnumCastTest.php +++ b/tests/Casts/EnumCastTest.php @@ -3,7 +3,6 @@ use Spatie\LaravelData\Casts\EnumCast; use Spatie\LaravelData\Casts\Uncastable; use Spatie\LaravelData\Support\Creation\CreationContextFactory; -use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\Enums\DummyUnitEnum; diff --git a/tests/Factories/FakeDataStructureFactory.php b/tests/Factories/FakeDataStructureFactory.php index 82c325cf..3739e946 100644 --- a/tests/Factories/FakeDataStructureFactory.php +++ b/tests/Factories/FakeDataStructureFactory.php @@ -45,8 +45,7 @@ public static function class( public static function method( ReflectionMethod $method, - ): DataMethod - { + ): DataMethod { $factory = static::$methodFactory ??= app(DataMethodFactory::class); return $factory->build($method, $method->getDeclaringClass()); @@ -55,8 +54,7 @@ public static function method( public static function constructor( ReflectionMethod $method, Collection $properties - ): DataMethod - { + ): DataMethod { $factory = static::$methodFactory ??= app(DataMethodFactory::class); return $factory->buildConstructor($method, $method->getDeclaringClass(), $properties); diff --git a/tests/RuleInferrers/RequiredRuleInferrerTest.php b/tests/RuleInferrers/RequiredRuleInferrerTest.php index 654a7127..0b2407aa 100644 --- a/tests/RuleInferrers/RequiredRuleInferrerTest.php +++ b/tests/RuleInferrers/RequiredRuleInferrerTest.php @@ -12,7 +12,6 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Optional; use Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer; -use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\Validation\PropertyRules; use Spatie\LaravelData\Support\Validation\RuleDenormalizer; use Spatie\LaravelData\Support\Validation\ValidationContext; diff --git a/tests/Support/DataClassTest.php b/tests/Support/DataClassTest.php index 458af352..ac12601e 100644 --- a/tests/Support/DataClassTest.php +++ b/tests/Support/DataClassTest.php @@ -6,7 +6,6 @@ use Spatie\LaravelData\Casts\DateTimeInterfaceCast; use Spatie\LaravelData\Data; use Spatie\LaravelData\Mappers\SnakeCaseMapper; -use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMapper; diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index 6c3da41b..7e5049f3 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -3,9 +3,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; -use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataType; -use Spatie\LaravelData\Support\OldTypes\OldType; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php index c68b5ca4..75454fb7 100644 --- a/tests/Support/DataReturnTypeTest.php +++ b/tests/Support/DataReturnTypeTest.php @@ -14,14 +14,11 @@ use Spatie\LaravelData\Contracts\ResponsableData; use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Contracts\WrappableData; -use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataReturnType; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; use Spatie\LaravelData\Support\Types\NamedType; -use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; -use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; class TestReturnTypeSubject @@ -57,7 +54,7 @@ public function dataCollection(): DataCollection expect($factory->buildFromNamedType($typeName))->toEqual($expected); expect($factory->buildFromValue($value))->toEqual($expected); -})->with(function (){ +})->with(function () { yield 'array' => [ 'methodName' => 'array', 'typeName' => 'array', @@ -110,7 +107,7 @@ public function dataCollection(): DataCollection TransformableData::class, ResponsableData::class, BaseDataCollectable::class, - Responsable::class + Responsable::class, ], DataTypeKind::DataCollection, null, null), kind: DataTypeKind::DataCollection, diff --git a/tests/Transformers/DateTimeInterfaceTransformerTest.php b/tests/Transformers/DateTimeInterfaceTransformerTest.php index cc260c11..94431a22 100644 --- a/tests/Transformers/DateTimeInterfaceTransformerTest.php +++ b/tests/Transformers/DateTimeInterfaceTransformerTest.php @@ -3,7 +3,6 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Spatie\LaravelData\Data; -use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; diff --git a/tests/Transformers/EnumTransformerTest.php b/tests/Transformers/EnumTransformerTest.php index 8046c277..97710511 100644 --- a/tests/Transformers/EnumTransformerTest.php +++ b/tests/Transformers/EnumTransformerTest.php @@ -1,7 +1,6 @@ Date: Fri, 19 Jan 2024 11:37:30 +0100 Subject: [PATCH 083/124] Better benchmarks --- benchmarks/DataBench.php | 212 ++++++++++++++++------- benchmarks/SimpleDataBench.php | 77 -------- benchmarks/SimpleDataCollectionBench.php | 77 -------- benchmarks/TestBench.php | 60 ------- 4 files changed, 152 insertions(+), 274 deletions(-) delete mode 100644 benchmarks/SimpleDataBench.php delete mode 100644 benchmarks/SimpleDataCollectionBench.php delete mode 100644 benchmarks/TestBench.php diff --git a/benchmarks/DataBench.php b/benchmarks/DataBench.php index 3b15d3f5..d7dbbf72 100644 --- a/benchmarks/DataBench.php +++ b/benchmarks/DataBench.php @@ -3,27 +3,38 @@ use Carbon\CarbonImmutable; use Illuminate\Support\Collection; use Orchestra\Testbench\Concerns\CreatesApplication; +use PhpBench\Attributes\AfterMethods; use PhpBench\Attributes\BeforeMethods; use PhpBench\Attributes\Iterations; use PhpBench\Attributes\Revs; -use PhpBench\Benchmark\Metadata\Annotations\Subject; +use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\LaravelDataServiceProvider; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; -use Spatie\LaravelData\Tests\Fakes\MultiNestedData; use Spatie\LaravelData\Tests\Fakes\NestedData; use Spatie\LaravelData\Tests\Fakes\SimpleData; -use function Amp\Iterator\toArray; class DataBench { use CreatesApplication; + protected DataCollection $collection; + + protected Data $object; + + protected array $collectionPayload; + + protected array $objectPayload; + + private DataConfig $dataConfig; + public function __construct() { $this->createApplication(); + $this->dataConfig = app(DataConfig::class); } protected function getPackageProviders($app) @@ -33,46 +44,76 @@ protected function getPackageProviders($app) ]; } - public function setup() + public function setupCache() { - app(DataConfig::class)->getDataClass(ComplicatedData::class)->prepareForCache(); - app(DataConfig::class)->getDataClass(SimpleData::class)->prepareForCache(); - app(DataConfig::class)->getDataClass(MultiNestedData::class)->prepareForCache(); - app(DataConfig::class)->getDataClass(NestedData::class)->prepareForCache(); + $this->dataConfig->getDataClass(ComplicatedData::class)->prepareForCache(); + $this->dataConfig->getDataClass(SimpleData::class)->prepareForCache(); + $this->dataConfig->getDataClass(NestedData::class)->prepareForCache(); } - #[Revs(500), Iterations(2)] - public function benchDataCreation() + public function setupCollectionTransformation() { - MultiNestedData::from([ - 'nested' => ['simple' => 'Hello'], - 'nestedCollection' => [ - ['simple' => 'I'], - ['simple' => 'am'], - ['simple' => 'groot'], - ], - ]); + $collection = Collection::times( + 15, + fn () => new ComplicatedData( + 42, + 42, + true, + 3.14, + 'Hello World', + [1, 1, 2, 3, 5, 8], + null, + Optional::create(), + 42, + CarbonImmutable::create(1994, 05, 16), + new DateTime('1994-05-16T12:00:00+01:00'), + new SimpleData('hello'), + new DataCollection(NestedData::class, [ + new NestedData(new SimpleData('I')), + new NestedData(new SimpleData('am')), + new NestedData(new SimpleData('groot')), + ]), + [ + new NestedData(new SimpleData('I')), + new NestedData(new SimpleData('am')), + new NestedData(new SimpleData('groot')), + ], + )); + + $this->collection = new DataCollection(ComplicatedData::class, $collection); } - #[Revs(500), Iterations(2)] - public function benchDataTransformation() + public function setupObjectTransformation() { - $data = new MultiNestedData( - new NestedData(new SimpleData('Hello')), + $this->object = new ComplicatedData( + 42, + 42, + true, + 3.14, + 'Hello World', + [1, 1, 2, 3, 5, 8], + null, + Optional::create(), + 42, + CarbonImmutable::create(1994, 05, 16), + new DateTime('1994-05-16T12:00:00+01:00'), + new SimpleData('hello'), + new DataCollection(NestedData::class, [ + new NestedData(new SimpleData('I')), + new NestedData(new SimpleData('am')), + new NestedData(new SimpleData('groot')), + ]), [ new NestedData(new SimpleData('I')), new NestedData(new SimpleData('am')), new NestedData(new SimpleData('groot')), - ] + ], ); - - $data->toArray(); } - #[Revs(500), Iterations(2)] - public function benchDataCollectionCreation() + public function setupCollectionCreation() { - $collection = Collection::times( + $this->collectionPayload = Collection::times( 15, fn() => [ 'withoutType' => 42, @@ -104,43 +145,94 @@ public function benchDataCollectionCreation() ], ] )->all(); + } - ComplicatedData::collect($collection, DataCollection::class); + public function setupObjectCreation() + { + $this->objectPayload = [ + 'withoutType' => 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => '16-06-1994', + 'defaultCast' => '1994-05-16T12:00:00+01:00', + 'nestedData' => [ + 'string' => 'hello', + ], + 'nestedCollection' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], + 'nestedArray' => [ + ['string' => 'never'], + ['string' => 'gonna'], + ['string' => 'give'], + ['string' => 'you'], + ['string' => 'up'], + ], + ]; } - #[Revs(500), Iterations(2)] - public function benchDataCollectionTransformation() + #[Revs(500), Iterations(5), BeforeMethods(['setupCache', 'setupCollectionTransformation'])] + public function benchCollectionTransformation() { - $collection = Collection::times( - 15, - fn() => new ComplicatedData( - 42, - 42, - true, - 3.14, - 'Hello World', - [1, 1, 2, 3, 5, 8], - null, - Optional::create(), - 42, - CarbonImmutable::create(1994,05,16), - new DateTime('1994-05-16T12:00:00+01:00'), - new SimpleData('hello'), - new DataCollection(NestedData::class, [ - new NestedData(new SimpleData('I')), - new NestedData(new SimpleData('am')), - new NestedData(new SimpleData('groot')), - ]), - [ - new NestedData(new SimpleData('I')), - new NestedData(new SimpleData('am')), - new NestedData(new SimpleData('groot')), - ], - ) - )->all(); + $this->collection->toArray(); + } + + #[Revs(5000), Iterations(5), BeforeMethods(['setupCache', 'setupObjectTransformation'])] + public function benchObjectTransformation() + { + $this->object->toArray(); + } + + #[Revs(500), Iterations(5), BeforeMethods(['setupCache', 'setupCollectionCreation'])] + public function benchCollectionCreation() + { + ComplicatedData::collect($this->collectionPayload, DataCollection::class); + } + + #[Revs(5000), Iterations(5), BeforeMethods(['setupCache', 'setupObjectCreation'])] + public function benchObjectCreation() + { + ComplicatedData::from($this->objectPayload); + } + + #[Revs(500), Iterations(5), BeforeMethods(['setupCollectionTransformation'])] + public function benchCollectionTransformationWithoutCache() + { + $this->collection->toArray(); - $dataCollection = (new DataCollection(ComplicatedData::class, $collection)); + $this->dataConfig->reset(); + } + + #[Revs(5000), Iterations(5), BeforeMethods(['setupObjectTransformation'])] + public function benchObjectTransformationWithoutCache() + { + $this->object->toArray(); + + $this->dataConfig->reset(); + } + + #[Revs(500), Iterations(5), BeforeMethods(['setupCollectionCreation'])] + public function benchCollectionCreationWithoutCache() + { + ComplicatedData::collect($this->collectionPayload, DataCollection::class); + + $this->dataConfig->reset(); + } + + #[Revs(5000), Iterations(5), BeforeMethods(['setupObjectCreation'])] + public function benchObjectCreationWithoutCache() + { + ComplicatedData::from($this->objectPayload); - $dataCollection->toArray(); + $this->dataConfig->reset(); } } diff --git a/benchmarks/SimpleDataBench.php b/benchmarks/SimpleDataBench.php deleted file mode 100644 index a0d7bde3..00000000 --- a/benchmarks/SimpleDataBench.php +++ /dev/null @@ -1,77 +0,0 @@ -createApplication(); - } - - protected function getPackageProviders($app) - { - return [ - LaravelDataServiceProvider::class, - ]; - } - - public function setup() - { - $this->data = new ComplicatedData( - 42, - 42, - true, - 3.14, - 'Hello World', - [1, 1, 2, 3, 5, 8], - null, - Optional::create(), - 42, - CarbonImmutable::create(1994, 05, 16), - new DateTime('1994-05-16T12:00:00+01:00'), - null, - null, - [] -// new SimpleData('hello'), -// new DataCollection(NestedData::class, [ -// new NestedData(new SimpleData('I')), -// new NestedData(new SimpleData('am')), -// new NestedData(new SimpleData('groot')), -// ]), -// [ -// new NestedData(new SimpleData('I')), -// new NestedData(new SimpleData('am')), -// new NestedData(new SimpleData('groot')), -// ], - ); - - app(DataConfig::class)->getDataClass(ComplicatedData::class); - app(DataConfig::class)->getDataClass(SimpleData::class); - } - - #[Revs(5000), Iterations(5), BeforeMethods('setup')] - public function benchDataTransformation() - { - $this->data->toArray(); - } - - #[Revs(5000), Iterations(5), BeforeMethods('setup')] - public function benchDataManualTransformation() - { - $this->data->toUserDefinedToArray(); - } -} diff --git a/benchmarks/SimpleDataCollectionBench.php b/benchmarks/SimpleDataCollectionBench.php deleted file mode 100644 index 75d7f963..00000000 --- a/benchmarks/SimpleDataCollectionBench.php +++ /dev/null @@ -1,77 +0,0 @@ -createApplication(); - } - - protected function getPackageProviders($app) - { - return [ - LaravelDataServiceProvider::class, - ]; - } - - public function setup() - { - $collection = Collection::times( - 15, - fn () => new ComplicatedData( - 42, - 42, - true, - 3.14, - 'Hello World', - [1, 1, 2, 3, 5, 8], - null, - Optional::create(), - 42, - CarbonImmutable::create(1994, 05, 16), - new DateTime('1994-05-16T12:00:00+01:00'), - null, - null, - [] -// new SimpleData('hello'), -// new DataCollection(NestedData::class, [ -// new NestedData(new SimpleData('I')), -// new NestedData(new SimpleData('am')), -// new NestedData(new SimpleData('groot')), -// ]), -// [ -// new NestedData(new SimpleData('I')), -// new NestedData(new SimpleData('am')), -// new NestedData(new SimpleData('groot')), -// ], - )); - - $this->dataCollection = new DataCollection(ComplicatedData::class, $collection); - - app(DataConfig::class)->getDataClass(ComplicatedData::class); - app(DataConfig::class)->getDataClass(SimpleData::class); - } - - #[Revs(500), Iterations(5), BeforeMethods('setup')] - public function benchDataCollectionTransformation() - { - $this->dataCollection->toArray(); - } -} diff --git a/benchmarks/TestBench.php b/benchmarks/TestBench.php deleted file mode 100644 index 866c7c99..00000000 --- a/benchmarks/TestBench.php +++ /dev/null @@ -1,60 +0,0 @@ -createApplication(); - } - - protected function getPackageProviders($app) - { - return [ - LaravelDataServiceProvider::class, - ]; - } - - #[Revs(5000), Iterations(5)] - public function benchUseStored() - { - for ($i = 0; $i < 100; $i++) { - $this->runStored(); - } - } - - protected function runStored(): array - { - return AcceptedTypesStorage::getAcceptedTypes(Collection::class); - } - - #[Revs(5000), Iterations(5)] - public function benchUseNative() - { - for ($i = 0; $i < 100; $i++) { - $this->runNative(); - } - } - - protected function runNative(): array - { - return ! class_exists(Collection::class) ? [] : array_unique([ - ...array_values(class_parents(Collection::class)), - ...array_values(class_implements(Collection::class)), - ]); - } -} From 994836df71a1c7ac7d83938ad8d8638d435f54af Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 11:49:33 +0100 Subject: [PATCH 084/124] Asert benchmarks --- benchmarks/DataBench.php | 61 ++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/benchmarks/DataBench.php b/benchmarks/DataBench.php index d7dbbf72..fd956c68 100644 --- a/benchmarks/DataBench.php +++ b/benchmarks/DataBench.php @@ -3,7 +3,7 @@ use Carbon\CarbonImmutable; use Illuminate\Support\Collection; use Orchestra\Testbench\Concerns\CreatesApplication; -use PhpBench\Attributes\AfterMethods; +use PhpBench\Attributes\Assert; use PhpBench\Attributes\BeforeMethods; use PhpBench\Attributes\Iterations; use PhpBench\Attributes\Revs; @@ -12,7 +12,6 @@ use Spatie\LaravelData\LaravelDataServiceProvider; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; use Spatie\LaravelData\Tests\Fakes\NestedData; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -115,7 +114,7 @@ public function setupCollectionCreation() { $this->collectionPayload = Collection::times( 15, - fn() => [ + fn () => [ 'withoutType' => 42, 'int' => 42, 'bool' => true, @@ -180,31 +179,56 @@ public function setupObjectCreation() ]; } - #[Revs(500), Iterations(5), BeforeMethods(['setupCache', 'setupCollectionTransformation'])] + #[ + Revs(500), + Iterations(5), + BeforeMethods(['setupCache', 'setupCollectionTransformation']), + Assert('mode(variant.time.avg) < 580 microseconds +/- 5%') + ] public function benchCollectionTransformation() { $this->collection->toArray(); } - #[Revs(5000), Iterations(5), BeforeMethods(['setupCache', 'setupObjectTransformation'])] + #[ + Revs(5000), + Iterations(5), + BeforeMethods(['setupCache', 'setupObjectTransformation']), + Assert('mode(variant.time.avg) < 38 microseconds +/- 5%') + ] public function benchObjectTransformation() { $this->object->toArray(); } - #[Revs(500), Iterations(5), BeforeMethods(['setupCache', 'setupCollectionCreation'])] + #[ + Revs(500), + Iterations(5), + BeforeMethods(['setupCache', 'setupCollectionCreation']), + Assert('mode(variant.time.avg) < 1.86 milliseconds +/- 5%') + ] public function benchCollectionCreation() { ComplicatedData::collect($this->collectionPayload, DataCollection::class); } - #[Revs(5000), Iterations(5), BeforeMethods(['setupCache', 'setupObjectCreation'])] + #[ + Revs(5000), + Iterations(5), + BeforeMethods(['setupCache', 'setupObjectCreation']), + Assert('mode(variant.time.avg) < 129 microseconds +/- 5%') + ] public function benchObjectCreation() { ComplicatedData::from($this->objectPayload); } - #[Revs(500), Iterations(5), BeforeMethods(['setupCollectionTransformation'])] + #[ + Revs(500), + Iterations(5), + BeforeMethods(['setupCollectionTransformation']), + Assert('mode(variant.time.avg) < 774 microseconds +/- 10%') + ] public function benchCollectionTransformationWithoutCache() { $this->collection->toArray(); @@ -212,7 +236,12 @@ public function benchCollectionTransformationWithoutCache() $this->dataConfig->reset(); } - #[Revs(5000), Iterations(5), BeforeMethods(['setupObjectTransformation'])] + #[ + Revs(5000), + Iterations(5), + BeforeMethods(['setupObjectTransformation']), + Assert('mode(variant.time.avg) < 217 microseconds +/- 10%') + ] public function benchObjectTransformationWithoutCache() { $this->object->toArray(); @@ -220,7 +249,12 @@ public function benchObjectTransformationWithoutCache() $this->dataConfig->reset(); } - #[Revs(500), Iterations(5), BeforeMethods(['setupCollectionCreation'])] + #[ + Revs(500), + Iterations(5), + BeforeMethods(['setupCollectionCreation']), + Assert('mode(variant.time.avg) < 2.15 milliseconds +/- 10%') + ] public function benchCollectionCreationWithoutCache() { ComplicatedData::collect($this->collectionPayload, DataCollection::class); @@ -228,7 +262,12 @@ public function benchCollectionCreationWithoutCache() $this->dataConfig->reset(); } - #[Revs(5000), Iterations(5), BeforeMethods(['setupObjectCreation'])] + #[ + Revs(5000), + Iterations(5), + BeforeMethods(['setupObjectCreation']), + Assert('mode(variant.time.avg) < 367 microseconds +/- 10%') + ] public function benchObjectCreationWithoutCache() { ComplicatedData::from($this->objectPayload); From 5ec98aa7fb7e134085306e7fc338f8a42b499d07 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 11:51:27 +0100 Subject: [PATCH 085/124] Update actions --- .github/workflows/benchmark.yml | 4 ++-- .github/workflows/phpstan.yml | 2 +- composer.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 5d247a23..e40d7c49 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -11,8 +11,8 @@ on: - 'phpbench.json' jobs: - phpstan: - name: phpstan + benchmarks: + name: Benchmarks runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 572ac9da..0f00b560 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -21,7 +21,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.1' + php-version: '8.2' coverage: none - name: Install composer dependencies diff --git a/composer.json b/composer.json index b2d15106..22dd8648 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require" : { - "php": "^8.2", + "php": "^8.1", "illuminate/contracts": "^10.0", "phpdocumentor/type-resolver": "^1.5", "spatie/laravel-package-tools": "^1.9.0", From b1bf84724355abaee6ef5e300e4f9e4122090726 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 12:17:30 +0100 Subject: [PATCH 086/124] Fix actions --- .github/workflows/benchmark.yml | 30 ------------------------------ tests/Support/DataTypeTest.php | 8 ++++---- 2 files changed, 4 insertions(+), 34 deletions(-) delete mode 100644 .github/workflows/benchmark.yml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml deleted file mode 100644 index e40d7c49..00000000 --- a/.github/workflows/benchmark.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Benchmarks - -on: - push: - paths: - - '**.php' - - 'phpbench.json' - pull_request: - paths: - - '**.php' - - 'phpbench.json' - -jobs: - benchmarks: - name: Benchmarks - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - coverage: none - - - name: Install composer dependencies - uses: ramsey/composer-install@v2 - - - name: Run Benchmark - run: composer benchmark diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataTypeTest.php index 09e09831..b2cdc420 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataTypeTest.php @@ -181,9 +181,9 @@ function resolveDataType(object $class, string $property = 'property'): DataType }); it('can deduce a nullable intersection type definition', function () { - $type = resolveDataType(new class () { - public (DateTime & DateTimeImmutable)|null $property; - }); + $code = '$type = resolveDataType(new class () {public (DateTime & DateTimeImmutable)|null $property;});'; + + eval($code); // We support PHP 8.1 which craches on this expect($type) ->isOptional->toBeFalse() @@ -200,7 +200,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType expect($type->type) ->toBeInstanceOf(IntersectionType::class); -}); +})->skipOnPhp('<8.2'); it('can deduce a mixed type', function () { $type = resolveDataType(new class () { From c7b4f9744400f1f915320e198ea3b9ad71df14d1 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 12:21:02 +0100 Subject: [PATCH 087/124] Fix actions --- .github/workflows/run-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 87590b7c..1e41dd50 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -14,7 +14,7 @@ jobs: stability: [prefer-lowest, prefer-stable] include: - laravel: 10.* - testbench: 8.* + testbench: ^8.20 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} From d122f0ad73ef27e4d1e5bd9275a0a2a2a14e5ebd Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 12:22:29 +0100 Subject: [PATCH 088/124] Fix actions --- .github/workflows/run-tests.yml | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 1e41dd50..2b9548f7 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,4 +1,4 @@ -name: run-tests +name: Run Tests on: ['push', 'pull_request'] diff --git a/composer.json b/composer.json index 22dd8648..53ac20b1 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "nette/php-generator": "^3.5", "nunomaduro/larastan": "^2.0", "orchestra/testbench": "^8.0", - "pestphp/pest": "^2.0", + "pestphp/pest": "^2.31", "pestphp/pest-plugin-laravel": "^2.0", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", From f82bd4c55e0cd1cfae7c4a0904265e24951dab37 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 13:02:11 +0100 Subject: [PATCH 089/124] Remove resolved partial --- composer.json | 2 +- src/Resolvers/VisibleDataFieldsResolver.php | 12 +- src/Support/Partials/Partial.php | 129 ++++++++++- src/Support/Partials/PartialsCollection.php | 14 +- src/Support/Partials/ResolvedPartial.php | 147 ------------- .../Partials/ResolvedPartialsCollection.php | 45 ---- src/Support/Transformation/DataContext.php | 12 +- .../Transformation/TransformationContext.php | 74 ++++--- .../TransformationContextFactory.php | 32 +-- .../VisibleDataFieldsResolverTest.php | 206 +++++++++--------- tests/Support/Partials/PartialTest.php | 141 ++++++++++++ .../Support/Partials/ResolvedPartialTest.php | 146 ------------- 12 files changed, 438 insertions(+), 522 deletions(-) delete mode 100644 src/Support/Partials/ResolvedPartial.php delete mode 100644 src/Support/Partials/ResolvedPartialsCollection.php delete mode 100644 tests/Support/Partials/ResolvedPartialTest.php diff --git a/composer.json b/composer.json index 53ac20b1..9d5ceaf0 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "test" : "./vendor/bin/pest --no-coverage", "test-coverage" : "vendor/bin/pest --coverage-html coverage", "format" : "vendor/bin/php-cs-fixer fix --allow-risky=yes", - "benchmark" : "vendor/bin/phpbench run --report=default", + "benchmark" : "vendor/bin/phpbench run", "benchmark-profiled" : "vendor/bin/phpbench " }, "config" : { diff --git a/src/Resolvers/VisibleDataFieldsResolver.php b/src/Resolvers/VisibleDataFieldsResolver.php index 3e21e5b4..71072d7c 100644 --- a/src/Resolvers/VisibleDataFieldsResolver.php +++ b/src/Resolvers/VisibleDataFieldsResolver.php @@ -131,7 +131,7 @@ protected function performExcept( if ($nested = $exceptPartial->getNested()) { try { - $fields[$nested]->addExceptResolvedPartial($exceptPartial->next()); + $fields[$nested]->addExceptPartial($exceptPartial->next()); } catch (ErrorException $exception) { $this->handleNonExistingNestedField($exception, PartialType::Except, $nested, $dataClass, $transformationContext); } @@ -169,7 +169,7 @@ protected function performOnly( if ($nested = $onlyPartial->getNested()) { try { - $fields[$nested]->addOnlyResolvedPartial($onlyPartial->next()); + $fields[$nested]->addOnlyPartial($onlyPartial->next()); $onlyFields[] = $nested; } catch (ErrorException $exception) { $this->handleNonExistingNestedField($exception, PartialType::Only, $nested, $dataClass, $transformationContext); @@ -220,7 +220,7 @@ protected function resolveIncludedFields( foreach ($includedFields as $includedField) { // can be null when field is a non data object/collectable or array - $fields[$includedField]?->addIncludedResolvedPartial($includedPartial->next()); + $fields[$includedField]?->addIncludedPartial($includedPartial->next()); } break; @@ -228,7 +228,7 @@ protected function resolveIncludedFields( if ($nested = $includedPartial->getNested()) { try { - $fields[$nested]->addIncludedResolvedPartial($includedPartial->next()); + $fields[$nested]->addIncludedPartial($includedPartial->next()); $includedFields[] = $nested; } catch (ErrorException $exception) { $this->handleNonExistingNestedField($exception, PartialType::Include, $nested, $dataClass, $transformationContext); @@ -268,7 +268,7 @@ protected function resolveExcludedFields( ->all(); foreach ($excludedFields as $excludedField) { - $fields[$excludedField]?->addExcludedResolvedPartial($excludePartial->next()); + $fields[$excludedField]?->addExcludedPartial($excludePartial->next()); } break; @@ -276,7 +276,7 @@ protected function resolveExcludedFields( if ($nested = $excludePartial->getNested()) { try { - $fields[$nested]->addExcludedResolvedPartial($excludePartial->next()); + $fields[$nested]->addExcludedPartial($excludePartial->next()); } catch (ErrorException $exception) { $this->handleNonExistingNestedField($exception, PartialType::Exclude, $nested, $dataClass, $transformationContext); } diff --git a/src/Support/Partials/Partial.php b/src/Support/Partials/Partial.php index 97a56792..615844c0 100644 --- a/src/Support/Partials/Partial.php +++ b/src/Support/Partials/Partial.php @@ -13,17 +13,23 @@ class Partial implements Stringable { - protected readonly ResolvedPartial $resolvedPartial; + protected int $segmentCount; + + protected bool $endsInAll; /** * @param array $segments */ public function __construct( public array $segments, - public bool $permanent, - public ?Closure $condition, + public bool $permanent = false, + public ?Closure $condition = null, + public int $pointer = 0, ) { - $this->resolvedPartial = new ResolvedPartial($segments); + $this->segmentCount = count($segments); + $this->endsInAll = $this->segmentCount === 0 + ? false + : $this->segments[$this->segmentCount - 1] instanceof AllPartialSegment; } public static function create( @@ -107,19 +113,120 @@ protected static function resolveSegmentsFromPath(string $path): array return $segments; } - public function resolve(BaseData|BaseDataCollectable $data): ?ResolvedPartial + public function isUndefined(): bool { - if ($this->condition === null) { - return $this->resolvedPartial->reset(); + return ! $this->endsInAll && $this->pointer >= $this->segmentCount; + } + + public function isAll(): bool + { + return $this->endsInAll && $this->pointer >= $this->segmentCount - 1; + } + + public function getNested(): ?string + { + $segment = $this->getCurrentSegment(); + + if ($segment === null) { + return null; + } + + if (! $segment instanceof NestedPartialSegment) { + return null; + } + + return $segment->field; + } + + public function getFields(): ?array + { + if ($this->isUndefined()) { + return null; + } + + $segment = $this->getCurrentSegment(); + + if ($segment === null) { + return null; } - if (($this->condition)($data)) { - return $this->resolvedPartial->reset(); + if (! $segment instanceof FieldsPartialSegment) { + return null; } - return null; + return $segment->fields; } + /** @return string[] */ + public function toLaravel(): array + { + /** @var array $segments */ + $segments = []; + + for ($i = $this->pointer; $i < $this->segmentCount; $i++) { + $segment = $this->segments[$i]; + + if ($segment instanceof AllPartialSegment) { + $segments[] = '*'; + + continue; + } + + if ($segment instanceof NestedPartialSegment) { + $segments[] = $segment->field; + + continue; + } + + if ($segment instanceof FieldsPartialSegment) { + $segmentsAsString = count($segments) === 0 + ? '' + : implode('.', $segments).'.'; + + return array_map( + fn (string $field) => "{$segmentsAsString}{$field}", + $segment->fields + ); + } + } + + return [implode('.', $segments)]; + } + + public function next(): self + { + $this->pointer++; + + return $this; + } + + public function rollbackWhenRequired(): void + { + $this->pointer--; + } + + public function reset(): self + { + $this->pointer = 0; + + return $this; + } + + protected function getCurrentSegment(): ?PartialSegment + { + return $this->segments[$this->pointer] ?? null; + } + + public function isRequired(BaseData|BaseDataCollectable $data): bool + { + if ($this->condition === null) { + return true; + } + + return ($this->condition)($data); + } + + public function toArray(): array { return [ @@ -131,6 +238,6 @@ public function toArray(): array public function __toString(): string { - return implode('.', $this->segments); + return implode('.', $this->segments)." (current: {$this->pointer})"; } } diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php index 24369647..7540b6d8 100644 --- a/src/Support/Partials/PartialsCollection.php +++ b/src/Support/Partials/PartialsCollection.php @@ -3,11 +3,12 @@ namespace Spatie\LaravelData\Support\Partials; use SplObjectStorage; +use Stringable; /** * @extends SplObjectStorage */ -class PartialsCollection extends SplObjectStorage +class PartialsCollection extends SplObjectStorage implements Stringable { public static function create(Partial ...$partials): self { @@ -30,4 +31,15 @@ public function toArray(): array return $output; } + + public function __toString(): string + { + $output = ''; + + foreach ($this as $partial) { + $output .= " - {$partial}".PHP_EOL; + } + + return $output; + } } diff --git a/src/Support/Partials/ResolvedPartial.php b/src/Support/Partials/ResolvedPartial.php deleted file mode 100644 index f898c94d..00000000 --- a/src/Support/Partials/ResolvedPartial.php +++ /dev/null @@ -1,147 +0,0 @@ - $segments - * @param int $pointer - */ - public function __construct( - public array $segments, - public int $pointer = 0, - ) { - $this->segmentCount = count($segments); - $this->endsInAll = $this->segmentCount === 0 - ? false - : $this->segments[$this->segmentCount - 1] instanceof AllPartialSegment; - } - - public function isUndefined(): bool - { - return ! $this->endsInAll && $this->pointer >= $this->segmentCount; - } - - public function isAll(): bool - { - return $this->endsInAll && $this->pointer >= $this->segmentCount - 1; - } - - public function getNested(): ?string - { - $segment = $this->getCurrentSegment(); - - if ($segment === null) { - return null; - } - - if (! $segment instanceof NestedPartialSegment) { - return null; - } - - return $segment->field; - } - - public function getFields(): ?array - { - if ($this->isUndefined()) { - return null; - } - - $segment = $this->getCurrentSegment(); - - if ($segment === null) { - return null; - } - - if (! $segment instanceof FieldsPartialSegment) { - return null; - } - - return $segment->fields; - } - - /** @return string[] */ - public function toLaravel(): array - { - /** @var array $segments */ - $segments = []; - - for ($i = $this->pointer; $i < $this->segmentCount; $i++) { - $segment = $this->segments[$i]; - - if ($segment instanceof AllPartialSegment) { - $segments[] = '*'; - - continue; - } - - if ($segment instanceof NestedPartialSegment) { - $segments[] = $segment->field; - - continue; - } - - if ($segment instanceof FieldsPartialSegment) { - $segmentsAsString = count($segments) === 0 - ? '' - : implode('.', $segments).'.'; - - return array_map( - fn (string $field) => "{$segmentsAsString}{$field}", - $segment->fields - ); - } - } - - return [implode('.', $segments)]; - } - - public function toArray(): array - { - return [ - 'segments' => $this->segments, - 'pointer' => $this->pointer, - ]; - } - - public function next(): self - { - $this->pointer++; - - return $this; - } - - public function rollbackWhenRequired(): void - { - $this->pointer--; - } - - public function reset(): self - { - $this->pointer = 0; - - return $this; - } - - protected function getCurrentSegment(): ?PartialSegment - { - return $this->segments[$this->pointer] ?? null; - } - - public function __toString(): string - { - return implode('.', $this->segments)." (current: {$this->pointer})"; - } -} diff --git a/src/Support/Partials/ResolvedPartialsCollection.php b/src/Support/Partials/ResolvedPartialsCollection.php deleted file mode 100644 index c0722496..00000000 --- a/src/Support/Partials/ResolvedPartialsCollection.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class ResolvedPartialsCollection extends SplObjectStorage implements Stringable -{ - public static function create(ResolvedPartial ...$resolvedPartials): self - { - $collection = new self(); - - foreach ($resolvedPartials as $resolvedPartial) { - $collection->attach($resolvedPartial); - } - - return $collection; - } - - public function toArray(): array - { - $output = []; - - foreach ($this as $resolvedPartial) { - $output[] = $resolvedPartial->toArray(); - } - - return $output; - } - - public function __toString(): string - { - $output = ''; - - foreach ($this as $partial) { - $output .= " - {$partial}".PHP_EOL; - } - - return $output; - } -} diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index dcdab050..59763594 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -48,16 +48,16 @@ public function mergePartials(DataContext $dataContext): self return $this; } - public function getResolvedPartialsAndRemoveTemporaryOnes( + public function getRequiredPartialsAndRemoveTemporaryOnes( BaseData|BaseDataCollectable $data, PartialsCollection $partials, - ): ResolvedPartialsCollection { - $resolvedPartials = new ResolvedPartialsCollection(); + ): PartialsCollection { + $requiredPartials = new PartialsCollection(); $partialsToDetach = new PartialsCollection(); foreach ($partials as $partial) { - if ($resolved = $partial->resolve($data)) { - $resolvedPartials->attach($resolved); + if ($partial->isRequired($data)) { + $requiredPartials->attach($partial->reset()); } if (! $partial->permanent) { @@ -67,6 +67,6 @@ public function getResolvedPartialsAndRemoveTemporaryOnes( $partials->removeAll($partialsToDetach); - return $resolvedPartials; + return $requiredPartials; } } diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index cde37e62..21fbbb0e 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -5,6 +5,8 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Contracts\IncludeableData; +use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; use Spatie\LaravelData\Support\Partials\ResolvedPartial; use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; @@ -20,10 +22,10 @@ public function __construct( public bool $mapPropertyNames = true, public WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, public ?GlobalTransformersCollection $transformers = null, - public ?ResolvedPartialsCollection $includePartials = null, - public ?ResolvedPartialsCollection $excludePartials = null, - public ?ResolvedPartialsCollection $onlyPartials = null, - public ?ResolvedPartialsCollection $exceptPartials = null, + public ?PartialsCollection $includePartials = null, + public ?PartialsCollection $excludePartials = null, + public ?PartialsCollection $onlyPartials = null, + public ?PartialsCollection $exceptPartials = null, ) { } @@ -34,81 +36,81 @@ public function setWrapExecutionType(WrapExecutionType $wrapExecutionType): self return $this; } - public function addIncludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void + public function addIncludedPartial(Partial ...$partials): void { if ($this->includePartials === null) { - $this->includePartials = new ResolvedPartialsCollection(); + $this->includePartials = new PartialsCollection(); } - foreach ($resolvedPartials as $resolvedPartial) { - $this->includePartials->attach($resolvedPartial); + foreach ($partials as $partial) { + $this->includePartials->attach($partial); } } - public function addExcludedResolvedPartial(ResolvedPartial ...$resolvedPartials): void + public function addExcludedPartial(Partial ...$partials): void { if ($this->excludePartials === null) { - $this->excludePartials = new ResolvedPartialsCollection(); + $this->excludePartials = new PartialsCollection(); } - foreach ($resolvedPartials as $resolvedPartial) { - $this->excludePartials->attach($resolvedPartial); + foreach ($partials as $partial) { + $this->excludePartials->attach($partial); } } - public function addOnlyResolvedPartial(ResolvedPartial ...$resolvedPartials): void + public function addOnlyPartial(Partial ...$partials): void { if ($this->onlyPartials === null) { - $this->onlyPartials = new ResolvedPartialsCollection(); + $this->onlyPartials = new PartialsCollection(); } - foreach ($resolvedPartials as $resolvedPartial) { - $this->onlyPartials->attach($resolvedPartial); + foreach ($partials as $partial) { + $this->onlyPartials->attach($partial); } } - public function addExceptResolvedPartial(ResolvedPartial ...$resolvedPartials): void + public function addExceptPartial(Partial ...$partials): void { if ($this->exceptPartials === null) { - $this->exceptPartials = new ResolvedPartialsCollection(); + $this->exceptPartials = new PartialsCollection(); } - foreach ($resolvedPartials as $resolvedPartial) { - $this->exceptPartials->attach($resolvedPartial); + foreach ($partials as $partial) { + $this->exceptPartials->attach($partial); } } - public function mergeIncludedResolvedPartials(ResolvedPartialsCollection $partials): void + public function mergeIncludedPartials(PartialsCollection $partials): void { if ($this->includePartials === null) { - $this->includePartials = new ResolvedPartialsCollection(); + $this->includePartials = new PartialsCollection(); } $this->includePartials->addAll($partials); } - public function mergeExcludedResolvedPartials(ResolvedPartialsCollection $partials): void + public function mergeExcludedPartials(PartialsCollection $partials): void { if ($this->excludePartials === null) { - $this->excludePartials = new ResolvedPartialsCollection(); + $this->excludePartials = new PartialsCollection(); } $this->excludePartials->addAll($partials); } - public function mergeOnlyResolvedPartials(ResolvedPartialsCollection $partials): void + public function mergeOnlyPartials(PartialsCollection $partials): void { if ($this->onlyPartials === null) { - $this->onlyPartials = new ResolvedPartialsCollection(); + $this->onlyPartials = new PartialsCollection(); } $this->onlyPartials->addAll($partials); } - public function mergeExceptResolvedPartials(ResolvedPartialsCollection $partials): void + public function mergeExceptPartials(PartialsCollection $partials): void { if ($this->exceptPartials === null) { - $this->exceptPartials = new ResolvedPartialsCollection(); + $this->exceptPartials = new PartialsCollection(); } $this->exceptPartials->addAll($partials); @@ -150,26 +152,26 @@ public function mergePartialsFromDataContext( $dataContext = $data->getDataContext(); if ($dataContext->includePartials && $dataContext->includePartials->count() > 0) { - $this->mergeIncludedResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->includePartials) + $this->mergeIncludedPartials( + $dataContext->getRequiredPartialsAndRemoveTemporaryOnes($data, $dataContext->includePartials) ); } if ($dataContext->excludePartials && $dataContext->excludePartials->count() > 0) { - $this->mergeExcludedResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->excludePartials) + $this->mergeExcludedPartials( + $dataContext->getRequiredPartialsAndRemoveTemporaryOnes($data, $dataContext->excludePartials) ); } if ($dataContext->onlyPartials && $dataContext->onlyPartials->count() > 0) { - $this->mergeOnlyResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->onlyPartials) + $this->mergeOnlyPartials( + $dataContext->getRequiredPartialsAndRemoveTemporaryOnes($data, $dataContext->onlyPartials) ); } if ($dataContext->exceptPartials && $dataContext->exceptPartials->count() > 0) { - $this->mergeExceptResolvedPartials( - $dataContext->getResolvedPartialsAndRemoveTemporaryOnes($data, $dataContext->exceptPartials) + $this->mergeExceptPartials( + $dataContext->getRequiredPartialsAndRemoveTemporaryOnes($data, $dataContext->exceptPartials) ); } diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index f53ce067..7b276ce1 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -38,13 +38,11 @@ public function get( $includePartials = null; if ($this->includePartials) { - $includePartials = new ResolvedPartialsCollection(); + $includePartials = new PartialsCollection(); foreach ($this->includePartials as $include) { - $resolved = $include->resolve($data); - - if ($resolved) { - $includePartials->attach($resolved); + if ($include->isRequired($data)) { + $includePartials->attach($include->reset()); } } } @@ -52,13 +50,11 @@ public function get( $excludePartials = null; if ($this->excludePartials) { - $excludePartials = new ResolvedPartialsCollection(); + $excludePartials = new PartialsCollection(); foreach ($this->excludePartials as $exclude) { - $resolved = $exclude->resolve($data); - - if ($resolved) { - $excludePartials->attach($resolved); + if ($exclude->isRequired($data)) { + $excludePartials->attach($exclude->reset()); } } } @@ -66,13 +62,11 @@ public function get( $onlyPartials = null; if ($this->onlyPartials) { - $onlyPartials = new ResolvedPartialsCollection(); + $onlyPartials = new PartialsCollection(); foreach ($this->onlyPartials as $only) { - $resolved = $only->resolve($data); - - if ($resolved) { - $onlyPartials->attach($resolved); + if ($only->isRequired($data)) { + $onlyPartials->attach($only->reset()); } } } @@ -80,13 +74,11 @@ public function get( $exceptPartials = null; if ($this->exceptPartials) { - $exceptPartials = new ResolvedPartialsCollection(); + $exceptPartials = new PartialsCollection(); foreach ($this->exceptPartials as $except) { - $resolved = $except->resolve($data); - - if ($resolved) { - $exceptPartials->attach($resolved); + if ($except->isRequired($data)) { + $exceptPartials->attach($except->reset()); } } } diff --git a/tests/Resolvers/VisibleDataFieldsResolverTest.php b/tests/Resolvers/VisibleDataFieldsResolverTest.php index 3bba1b62..82f8083b 100644 --- a/tests/Resolvers/VisibleDataFieldsResolverTest.php +++ b/tests/Resolvers/VisibleDataFieldsResolverTest.php @@ -11,8 +11,8 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; -use Spatie\LaravelData\Support\Partials\ResolvedPartial; -use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; +use Spatie\LaravelData\Support\Partials\Partial; +use Spatie\LaravelData\Support\Partials\PartialsCollection; use Spatie\LaravelData\Support\Partials\Segments\AllPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\FieldsPartialSegment; use Spatie\LaravelData\Support\Partials\Segments\NestedPartialSegment; @@ -396,8 +396,8 @@ public static function instance(): self ->except('nested.a'), 'fields' => [ 'nested' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], pointer: 1) ), ), ], @@ -414,8 +414,8 @@ public static function instance(): self ->except('nested.{a,b}'), 'fields' => [ 'nested' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], pointer: 1) ), ), ], @@ -430,8 +430,8 @@ public static function instance(): self ->except('nested.*'), 'fields' => [ 'nested' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -446,8 +446,8 @@ public static function instance(): self ->except('collection.string'), 'fields' => [ 'collection' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -465,8 +465,8 @@ public static function instance(): self ->except('collection.{string,int}'), 'fields' => [ 'collection' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], pointer: 1) ), ), ], @@ -484,8 +484,8 @@ public static function instance(): self ->except('collection.*'), 'fields' => [ 'collection' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -504,18 +504,18 @@ public static function instance(): self ->except('nested.a.string'), 'fields' => [ 'single' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'collection' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'nested' => new TransformationContext( - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -605,8 +605,8 @@ public static function instance(): self ->only('nested.a'), 'fields' => [ 'nested' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], pointer: 1) ), ), ], @@ -622,8 +622,8 @@ public static function instance(): self ->only('nested.{a,b}'), 'fields' => [ 'nested' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], pointer: 1) ), ), ], @@ -640,8 +640,8 @@ public static function instance(): self ->only('nested.*'), 'fields' => [ 'nested' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -658,8 +658,8 @@ public static function instance(): self ->only('collection.string'), 'fields' => [ 'collection' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -676,8 +676,8 @@ public static function instance(): self ->only('collection.{string,int}'), 'fields' => [ 'collection' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], pointer: 1) ), ), ], @@ -694,8 +694,8 @@ public static function instance(): self ->only('collection.*'), 'fields' => [ 'collection' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -715,18 +715,18 @@ public static function instance(): self 'fields' => [ 'string' => null, 'single' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'collection' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'nested' => new TransformationContext( - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -852,20 +852,20 @@ public static function instance(bool $includeByDefault): self ->include('*'), 'fields' => [ 'single' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new AllPartialSegment()], 3) + includePartials: PartialsCollection::create( + new Partial([new AllPartialSegment()], pointer: 3) ), ), 'int' => null, 'string' => null, 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new AllPartialSegment()], 3) + includePartials: PartialsCollection::create( + new Partial([new AllPartialSegment()], pointer: 3) ), ), 'collection' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new AllPartialSegment()], 3) + includePartials: PartialsCollection::create( + new Partial([new AllPartialSegment()], pointer: 3) ), ), ], @@ -890,8 +890,8 @@ public static function instance(bool $includeByDefault): self ->include('nested.a'), 'fields' => [ 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], pointer: 1) ), ), ], @@ -908,8 +908,8 @@ public static function instance(bool $includeByDefault): self ->include('nested.{a,b}'), 'fields' => [ 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], pointer: 1) ), ), ], @@ -927,8 +927,8 @@ public static function instance(bool $includeByDefault): self ->include('nested.*'), 'fields' => [ 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -946,9 +946,9 @@ public static function instance(bool $includeByDefault): self ->include('nested.a.string', 'nested.b.int'), 'fields' => [ 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1), - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1), + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], pointer: 1) ), ), ], @@ -966,8 +966,8 @@ public static function instance(bool $includeByDefault): self ->include('collection.string'), 'fields' => [ 'collection' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -985,8 +985,8 @@ public static function instance(bool $includeByDefault): self ->include('collection.{string,int}'), 'fields' => [ 'collection' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], pointer: 1) ), ), ], @@ -1004,8 +1004,8 @@ public static function instance(bool $includeByDefault): self ->include('collection.*'), 'fields' => [ 'collection' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -1025,18 +1025,18 @@ public static function instance(bool $includeByDefault): self 'fields' => [ 'string' => null, 'single' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'collection' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -1125,8 +1125,8 @@ public static function instance(bool $includeByDefault): self ->exclude('nested.a'), 'fields' => [ 'nested' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a'])], pointer: 1) ), ), ], @@ -1143,8 +1143,8 @@ public static function instance(bool $includeByDefault): self ->exclude('nested.{a,b}'), 'fields' => [ 'nested' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new FieldsPartialSegment(['a', 'b'])], pointer: 1) ), ), ], @@ -1159,8 +1159,8 @@ public static function instance(bool $includeByDefault): self ->exclude('nested.*'), 'fields' => [ 'nested' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -1175,9 +1175,9 @@ public static function instance(bool $includeByDefault): self ->exclude('nested.a.string', 'nested.b.int'), 'fields' => [ 'nested' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1), - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1), + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], pointer: 1) ), ), ], @@ -1195,8 +1195,8 @@ public static function instance(bool $includeByDefault): self ->exclude('collection.string'), 'fields' => [ 'collection' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), ], @@ -1214,8 +1214,8 @@ public static function instance(bool $includeByDefault): self ->exclude('collection.{string,int}'), 'fields' => [ 'collection' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string', 'int'])], pointer: 1) ), ), ], @@ -1233,8 +1233,8 @@ public static function instance(bool $includeByDefault): self ->exclude('collection.*'), 'fields' => [ 'collection' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new AllPartialSegment()], pointer: 1) ), ), ], @@ -1253,18 +1253,18 @@ public static function instance(bool $includeByDefault): self ->exclude('nested.a.string'), 'fields' => [ 'single' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('single'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'collection' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'nested' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1) ), ), 'int' => null, @@ -1304,34 +1304,34 @@ public static function instance(bool $includeByDefault): self $expectedVisibleFields = [ 'single' => new TransformationContext( - excludePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('single'), new FieldsPartialSegment(['int'])], 1) + excludePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('single'), new FieldsPartialSegment(['int'])], pointer: 1) ), - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('single'), new AllPartialSegment()], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('single'), new AllPartialSegment()], pointer: 1) ), ), 'nested' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], 1), - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new AllPartialSegment()], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('a'), new FieldsPartialSegment(['string'])], pointer: 1), + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new AllPartialSegment()], pointer: 1) ), - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new AllPartialSegment()], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new AllPartialSegment()], pointer: 1) ), - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('nested'), new NestedPartialSegment('b'), new FieldsPartialSegment(['int'])], pointer: 1) ), ), 'collection' => new TransformationContext( - includePartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], 1) + includePartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['string'])], pointer: 1) ), - onlyPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new AllPartialSegment()], 1) + onlyPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new AllPartialSegment()], pointer: 1) ), - exceptPartials: ResolvedPartialsCollection::create( - new ResolvedPartial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['int'])], 1) + exceptPartials: PartialsCollection::create( + new Partial([new NestedPartialSegment('collection'), new FieldsPartialSegment(['int'])], pointer: 1) ), ), ]; diff --git a/tests/Support/Partials/PartialTest.php b/tests/Support/Partials/PartialTest.php index 51bcf23e..764cec1d 100644 --- a/tests/Support/Partials/PartialTest.php +++ b/tests/Support/Partials/PartialTest.php @@ -66,3 +66,144 @@ function invalidPartialsProvider(): Generator 'expected' => [new FieldsPartialSegment(['name', 'age'])], ]; } + +it('can use the pointer system when ending in a field', function () { + $partial = new Partial([ + new NestedPartialSegment('struct'), + new FieldsPartialSegment(['name', 'age']), + ]); + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(['name', 'age']); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + $partial->next(); // level 2 - non existing + + expect($partial->isUndefined())->toBeTrue(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(['name', 'age']); + + $partial->next(); // level 2 - non existing + $partial->next(); // level 3 - non existing + + expect($partial->isUndefined())->toBeTrue(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 2 - non existing + + expect($partial->isUndefined())->toBeTrue(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(['name', 'age']); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); +}); + +it('can use the pointer system when ending in an all', function () { + $partial = new Partial([ + new NestedPartialSegment('struct'), + new AllPartialSegment(), + ]); + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 1 + $partial->next(); // level 2 - non existing + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->next(); // level 2 - non existing + $partial->next(); // level 3 - non existing + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 2 - non existing + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 1 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeTrue(); + expect($partial->getNested())->toBe(null); + expect($partial->getFields())->toBe(null); + + $partial->rollbackWhenRequired(); // level 0 + + expect($partial->isUndefined())->toBeFalse(); + expect($partial->isAll())->toBeFalse(); + expect($partial->getNested())->toBe('struct'); + expect($partial->getFields())->toBe(null); +}); + diff --git a/tests/Support/Partials/ResolvedPartialTest.php b/tests/Support/Partials/ResolvedPartialTest.php deleted file mode 100644 index 6cfd8874..00000000 --- a/tests/Support/Partials/ResolvedPartialTest.php +++ /dev/null @@ -1,146 +0,0 @@ -isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe('struct'); - expect($partial->getFields())->toBe(null); - - $partial->next(); // level 1 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(['name', 'age']); - - $partial->rollbackWhenRequired(); // level 0 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe('struct'); - expect($partial->getFields())->toBe(null); - - $partial->next(); // level 1 - $partial->next(); // level 2 - non existing - - expect($partial->isUndefined())->toBeTrue(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 1 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(['name', 'age']); - - $partial->next(); // level 2 - non existing - $partial->next(); // level 3 - non existing - - expect($partial->isUndefined())->toBeTrue(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 2 - non existing - - expect($partial->isUndefined())->toBeTrue(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 1 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(['name', 'age']); - - $partial->rollbackWhenRequired(); // level 0 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe('struct'); - expect($partial->getFields())->toBe(null); -}); - -it('can use the pointer system when ending in an all', function () { - $partial = new ResolvedPartial([ - new NestedPartialSegment('struct'), - new AllPartialSegment(), - ]); - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe('struct'); - expect($partial->getFields())->toBe(null); - - $partial->next(); // level 1 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeTrue(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 0 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe('struct'); - expect($partial->getFields())->toBe(null); - - $partial->next(); // level 1 - $partial->next(); // level 2 - non existing - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeTrue(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 1 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeTrue(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->next(); // level 2 - non existing - $partial->next(); // level 3 - non existing - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeTrue(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 2 - non existing - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeTrue(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 1 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeTrue(); - expect($partial->getNested())->toBe(null); - expect($partial->getFields())->toBe(null); - - $partial->rollbackWhenRequired(); // level 0 - - expect($partial->isUndefined())->toBeFalse(); - expect($partial->isAll())->toBeFalse(); - expect($partial->getNested())->toBe('struct'); - expect($partial->getFields())->toBe(null); -}); From 8bb91eb0c54d52b24de62939afb6347ac72a4ec6 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 19 Jan 2024 12:02:34 +0000 Subject: [PATCH 090/124] Fix styling --- src/Support/Partials/PartialsCollection.php | 2 +- src/Support/Transformation/DataContext.php | 1 - src/Support/Transformation/TransformationContext.php | 2 -- src/Support/Transformation/TransformationContextFactory.php | 1 - tests/Support/Partials/PartialTest.php | 1 - 5 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php index 7540b6d8..c3d47fb1 100644 --- a/src/Support/Partials/PartialsCollection.php +++ b/src/Support/Partials/PartialsCollection.php @@ -8,7 +8,7 @@ /** * @extends SplObjectStorage */ -class PartialsCollection extends SplObjectStorage implements Stringable +class PartialsCollection extends SplObjectStorage implements Stringable { public static function create(Partial ...$partials): self { diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index 59763594..52c0d7b0 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -5,7 +5,6 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Support\Partials\PartialsCollection; -use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\Wrap; class DataContext diff --git a/src/Support/Transformation/TransformationContext.php b/src/Support/Transformation/TransformationContext.php index 21fbbb0e..4f3265a8 100644 --- a/src/Support/Transformation/TransformationContext.php +++ b/src/Support/Transformation/TransformationContext.php @@ -7,8 +7,6 @@ use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Partials\PartialsCollection; -use Spatie\LaravelData\Support\Partials\ResolvedPartial; -use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Stringable; diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 7b276ce1..61bddd0d 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -7,7 +7,6 @@ use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Partials\PartialsCollection; -use Spatie\LaravelData\Support\Partials\ResolvedPartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Transformers\Transformer; diff --git a/tests/Support/Partials/PartialTest.php b/tests/Support/Partials/PartialTest.php index 764cec1d..325a5858 100644 --- a/tests/Support/Partials/PartialTest.php +++ b/tests/Support/Partials/PartialTest.php @@ -206,4 +206,3 @@ function invalidPartialsProvider(): Generator expect($partial->getNested())->toBe('struct'); expect($partial->getFields())->toBe(null); }); - From 5b34518c4f10de7c271b3dacaa757bcf34cd1c18 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 13:45:09 +0100 Subject: [PATCH 091/124] Speed improvements --- src/DataPipes/DataPipe.php | 9 +++++ src/DataPipes/DefaultValuesDataPipe.php | 35 ++++++++--------- .../FillRouteParameterPropertiesDataPipe.php | 1 - src/Exceptions/CannotCreateData.php | 4 +- src/Resolvers/DataFromArrayResolver.php | 38 +++++++++++-------- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/src/DataPipes/DataPipe.php b/src/DataPipes/DataPipe.php index c5ee8df1..91cdb69b 100644 --- a/src/DataPipes/DataPipe.php +++ b/src/DataPipes/DataPipe.php @@ -5,9 +5,18 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; +use Spatie\LaravelData\Support\DataProperty; interface DataPipe { + /** + * @param mixed $payload + * @param DataClass $class + * @param Collection $properties + * @param CreationContext $creationContext + * + * @return Collection + */ public function handle( mixed $payload, DataClass $class, diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index b865068b..da42cb3b 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -16,28 +16,29 @@ public function handle( Collection $properties, CreationContext $creationContext ): Collection { - $class - ->properties - ->filter(fn (DataProperty $property) => ! $properties->has($property->name)) - ->each(function (DataProperty $property) use (&$properties) { - if ($property->hasDefaultValue) { - $properties[$property->name] = $property->defaultValue; + foreach ($class->properties as $name => $property) { + if($properties->has($name)) { + continue; + } - return; - } + if ($property->hasDefaultValue) { + $properties[$name] = $property->defaultValue; - if ($property->type->isOptional) { - $properties[$property->name] = Optional::create(); + continue; + } - return; - } + if ($property->type->isOptional) { + $properties[$name] = Optional::create(); - if ($property->type->isNullable) { - $properties[$property->name] = null; + continue; + } - return; - } - }); + if ($property->type->isNullable) { + $properties[$name] = null; + + continue; + } + } return $properties; } diff --git a/src/DataPipes/FillRouteParameterPropertiesDataPipe.php b/src/DataPipes/FillRouteParameterPropertiesDataPipe.php index 394836ca..ca667e8f 100644 --- a/src/DataPipes/FillRouteParameterPropertiesDataPipe.php +++ b/src/DataPipes/FillRouteParameterPropertiesDataPipe.php @@ -65,6 +65,5 @@ protected function resolveValue( } return data_get($parameter, $attribute->property ?? $dataProperty->name); - ; } } diff --git a/src/Exceptions/CannotCreateData.php b/src/Exceptions/CannotCreateData.php index b7c48e8c..2a9f5a60 100644 --- a/src/Exceptions/CannotCreateData.php +++ b/src/Exceptions/CannotCreateData.php @@ -24,9 +24,11 @@ public static function noNormalizerFound(string $dataClass, mixed $value): self public static function constructorMissingParameters( DataClass $dataClass, - Collection $parameters, + array $parameters, Throwable $previous, ): self { + $parameters = collect($parameters); + $message = "Could not create `{$dataClass->name}`: the constructor requires {$dataClass->constructorMethod->parameters->count()} parameters, {$parameters->count()} given."; diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 33eb7e7d..469ec192 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -31,21 +31,7 @@ public function execute(string $class, Collection $properties): BaseData { $dataClass = $this->dataConfig->getDataClass($class); - $constructorParameters = $dataClass->constructorMethod?->parameters ?? collect(); - - $data = $constructorParameters - ->mapWithKeys(function (DataParameter|DataProperty $parameter) use ($properties) { - if ($properties->has($parameter->name)) { - return [$parameter->name => $properties->get($parameter->name)]; - } - - if (! $parameter->isPromoted && $parameter->hasDefaultValue) { - return [$parameter->name => $parameter->defaultValue]; - } - - return []; - }) - ->pipe(fn (Collection $parameters) => $this->createData($dataClass, $parameters)); + $data = $this->createData($dataClass, $properties); $dataClass ->properties @@ -81,8 +67,28 @@ public function execute(string $class, Collection $properties): BaseData protected function createData( DataClass $dataClass, - Collection $parameters, + Collection $properties, ) { + $constructorParameters = $dataClass->constructorMethod?->parameters; + + if ($constructorParameters === null) { + return new $dataClass->name(); + } + + $parameters = []; + + foreach ($constructorParameters as $parameter) { + if ($properties->has($parameter->name)) { + $parameters[$parameter->name] = $properties->get($parameter->name); + + continue; + } + + if (! $parameter->isPromoted && $parameter->hasDefaultValue) { + $parameters[$parameter->name] = $parameter->defaultValue; + } + } + try { return new $dataClass->name(...$parameters); } catch (ArgumentCountError $error) { From ee4dff3740db341ab2cc6be933c1f2cce465b302 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 19 Jan 2024 12:45:35 +0000 Subject: [PATCH 092/124] Fix styling --- src/DataPipes/DataPipe.php | 1 - src/DataPipes/DefaultValuesDataPipe.php | 1 - src/Exceptions/CannotCreateData.php | 1 - src/Resolvers/DataFromArrayResolver.php | 1 - 4 files changed, 4 deletions(-) diff --git a/src/DataPipes/DataPipe.php b/src/DataPipes/DataPipe.php index 91cdb69b..59bae51b 100644 --- a/src/DataPipes/DataPipe.php +++ b/src/DataPipes/DataPipe.php @@ -5,7 +5,6 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; -use Spatie\LaravelData\Support\DataProperty; interface DataPipe { diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index da42cb3b..56ff601c 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -6,7 +6,6 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; -use Spatie\LaravelData\Support\DataProperty; class DefaultValuesDataPipe implements DataPipe { diff --git a/src/Exceptions/CannotCreateData.php b/src/Exceptions/CannotCreateData.php index 2a9f5a60..11a2d2c0 100644 --- a/src/Exceptions/CannotCreateData.php +++ b/src/Exceptions/CannotCreateData.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Exceptions; use Exception; -use Illuminate\Support\Collection; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataProperty; diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 469ec192..45ae6207 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -10,7 +10,6 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataParameter; use Spatie\LaravelData\Support\DataProperty; /** From 2dc1de6f99f8b3718f5ff62ea3b1a585e3f1f8c7 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 14:08:55 +0100 Subject: [PATCH 093/124] wip --- benchmarks/DataProfileBench.php | 180 ++++++++++++++++++++++ composer.json | 4 +- src/Resolvers/TransformedDataResolver.php | 4 + 3 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 benchmarks/DataProfileBench.php diff --git a/benchmarks/DataProfileBench.php b/benchmarks/DataProfileBench.php new file mode 100644 index 00000000..84bfc86a --- /dev/null +++ b/benchmarks/DataProfileBench.php @@ -0,0 +1,180 @@ +createApplication(); + + $this->dataConfig = app(DataConfig::class); + + $this->setupCache();; + } + + protected function getPackageProviders($app) + { + return [ + LaravelDataServiceProvider::class, + ]; + } + + public function setupCache() + { + $this->dataConfig->getDataClass(ComplicatedData::class)->prepareForCache(); + $this->dataConfig->getDataClass(SimpleData::class)->prepareForCache(); + $this->dataConfig->getDataClass(NestedData::class)->prepareForCache(); + } + + public function setupCollectionTransformation() + { + $collection = Collection::times( + 15, + fn () => new ComplicatedData( + 42, + 42, + true, + 3.14, + 'Hello World', + [1, 1, 2, 3, 5, 8], + null, + Optional::create(), + 42, + CarbonImmutable::create(1994, 05, 16), + new DateTime('1994-05-16T12:00:00+01:00'), + null, + null, + [], + )); + + $this->collection = new DataCollection(ComplicatedData::class, $collection); + } + + public function setupObjectTransformation() + { + $this->object = new ComplicatedData( + 42, + 42, + true, + 3.14, + 'Hello World', + [1, 1, 2, 3, 5, 8], + null, + Optional::create(), + 42, + CarbonImmutable::create(1994, 05, 16), + new DateTime('1994-05-16T12:00:00+01:00'), + null, + null, + [], + ); + } + + public function setupCollectionCreation() + { + $this->collectionPayload = Collection::times( + 15, + fn () => [ + 'withoutType' => 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => '16-06-1994', + 'defaultCast' => '1994-05-16T12:00:00+01:00', + 'nestedData' => null, + 'nestedCollection' => null, + 'nestedArray' => [], + ] + )->all(); + } + + public function setupObjectCreation() + { + $this->objectPayload = [ + 'withoutType' => 42, + 'int' => 42, + 'bool' => true, + 'float' => 3.14, + 'string' => 'Hello world', + 'array' => [1, 1, 2, 3, 5, 8], + 'nullable' => null, + 'mixed' => 42, + 'explicitCast' => '16-06-1994', + 'defaultCast' => '1994-05-16T12:00:00+01:00', + 'nestedData' => null, + 'nestedCollection' => null, + 'nestedArray' => [], + ]; + } + + #[ + Revs(500), + Iterations(5), + BeforeMethods([ 'setupCollectionTransformation']), + ] + public function benchProfileCollectionTransformation() + { + $this->collection->toArray(); + } + + #[ + Revs(5000), + Iterations(5), + BeforeMethods([ 'setupObjectTransformation']), + ] + public function benchProfileObjectTransformation() + { + $this->object->toArray(); + } + + #[ + Revs(500), + Iterations(5), + BeforeMethods([ 'setupCollectionCreation']), + ] + public function benchProfileCollectionCreation() + { + ComplicatedData::collect($this->collectionPayload, DataCollection::class); + } + + #[ + Revs(5000), + Iterations(5), + BeforeMethods([ 'setupObjectCreation']), + ] + public function benchProfileObjectCreation() + { + ComplicatedData::from($this->objectPayload); + } +} diff --git a/composer.json b/composer.json index 9d5ceaf0..d88cf764 100644 --- a/composer.json +++ b/composer.json @@ -56,8 +56,8 @@ "test" : "./vendor/bin/pest --no-coverage", "test-coverage" : "vendor/bin/pest --coverage-html coverage", "format" : "vendor/bin/php-cs-fixer fix --allow-risky=yes", - "benchmark" : "vendor/bin/phpbench run", - "benchmark-profiled" : "vendor/bin/phpbench " + "benchmark" : "vendor/bin/phpbench run --filter=DataBench", + "profile" : "vendor/bin/phpbench xdebug:profile --filter=DataProfileBench" }, "config" : { "sort-packages" : true, diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 38559eae..3722b7c3 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -106,6 +106,10 @@ protected function resolvePropertyValue( return $this->resolvePotentialPartialArray($value, $fieldContext); } + if($property->type->kind === DataTypeKind::Default) { + return $value; // Done for performance reasons + } + if ( $value instanceof BaseDataCollectable && $value instanceof TransformableData From 03ec7cfc9be3ebf67e977bcbfe698cc190f70529 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 15:48:24 +0100 Subject: [PATCH 094/124] Speed improvements, upgrade guide, changelog --- CHANGELOG.md | 4 +- UPGRADING.md | 233 +++++++++++++++--- src/Casts/Cast.php | 2 +- src/Casts/DateTimeInterfaceCast.php | 3 +- src/Casts/EnumCast.php | 2 +- src/Concerns/BaseData.php | 2 +- src/Concerns/TransformableData.php | 2 +- src/Contracts/BaseData.php | 2 +- src/DataPipes/AuthorizedDataPipe.php | 4 +- src/DataPipes/CastPropertiesDataPipe.php | 6 +- src/DataPipes/DataPipe.php | 8 +- src/DataPipes/DefaultValuesDataPipe.php | 6 +- .../FillRouteParameterPropertiesDataPipe.php | 12 +- src/DataPipes/MapPropertiesDataPipe.php | 7 +- src/DataPipes/ValidatePropertiesDataPipe.php | 4 +- src/Resolvers/DataFromArrayResolver.php | 65 ++--- src/Resolvers/DataFromSomethingResolver.php | 2 +- ...=> TransformedDataCollectableResolver.php} | 2 +- src/Resolvers/TransformedDataResolver.php | 2 +- src/Support/DataContainer.php | 10 +- src/Support/ResolvedDataPipeline.php | 4 +- tests/Casts/DateTimeInterfaceCastTest.php | 28 +-- tests/Casts/EnumCastTest.php | 8 +- .../CastTransformers/FakeCastTransformer.php | 2 +- tests/Fakes/Castables/SimpleCastable.php | 2 +- tests/Fakes/Casts/ConfidentialDataCast.php | 2 +- .../Casts/ConfidentialDataCollectionCast.php | 2 +- tests/Fakes/Casts/ContextAwareCast.php | 4 +- tests/Fakes/Casts/MeaningOfLifeCast.php | 2 +- tests/Fakes/Casts/StringToUpperCast.php | 2 +- tests/PipelineTest.php | 5 +- tests/TestSupport/DataValidationAsserter.php | 2 +- 32 files changed, 294 insertions(+), 147 deletions(-) rename src/Resolvers/{TransformedDataCollectionResolver.php => TransformedDataCollectableResolver.php} (98%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c51edd63..f4a132ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ All notable changes to `laravel-data` will be documented in this file. - Addition of collect method - Removal of collection method - Add support for using Laravel Model attributes as data properties -- Add support for class defined defaults - Allow creating data objects using `from` without parameters - Add support for a Dto and Resource object +- Added contexts to the creation and transformation process +- Allow creating a data object or collection using a factory +- Speed up the process of creating and transforming data objects ## 3.11.0 - 2023-12-21 diff --git a/UPGRADING.md b/UPGRADING.md index 8460d00f..f941c40d 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,48 +1,189 @@ # Upgrading -Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. +Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not +cover. We accept PRs to improve this guide. ## From v3 to v4 The following things are required when upgrading: -- Laravel 10 is now required -- Start by going through your code and replace all static `SomeData::collection($items)` method calls with `SomeData::collect($items, DataCollection::class)` - - Use `DataPaginatedCollection::class` when you're expecting a paginated collection - - Use `DataCursorPaginatedCollection::class` when you're expecting a cursor paginated collection - - For a more gentle upgrade you can also use the `WithDeprecatedCollectionMethod` trait which adds the collection method again, but this trait will be removed in v5 - - If you were using `$_collectionClass`, `$_paginatedCollectionClass` or `$_cursorPaginatedCollectionClass` then take a look at the magic collect functionality on information about how to replace these -- If you were manually working with `$_includes`, ` $_excludes`, `$_only`, `$_except` or `$_wrap` these can now be found within the `$_dataContext` -- We split up some traits and interfaces, if you're manually using these on you own data implementation then take a look what has changed - - DataTrait (T) and PrepareableData (T/I) were removed - - 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 have implemented a custom `Cast` - - The `$castContext` parameter is renamed to `$properties` and changed it type from `array` to `collection` - - A new `$context` parameter is added of type `CreationContext` -- If you have implemented a custom DataPipe, update the `handle` method signature with the new `TransformationContext` parameter -- If you manually created `ValidatePropertiesDataPipe` using the `allTypes` parameter, please now use the creation context for this -- The `withoutMagicalCreationFrom` method was removed from data in favour for creation by factory -- 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 -- In previous versions, when trying to include, exclude, only or except certain data properties that did not exist not exception was thrown. This is now the case, these exceptions can be silenced by setting `ignore_invalid_partials` to true within the config file +**Dependency changes (Likelihood Of Impact: High)** + +The package now requires Laravel 10 as a minimum. + +**Data::collection removal (Likelihood Of Impact: High)** + +We've removed the Data::collection method in favour of the collect method. The collect method will return as type what +it gets as a type, so passing in an array will return an array of data objects. With the second parameter the output +type can be overwritten: + +```php +// v3 +SomeData::collection($items); // DataCollection + +// v4 +SomeData::collect($items); // array of SomeData + +SomeData::collect($items, DataCollection::class); // DataCollection +``` + +If you were using the `$_collectionClass`, `$_paginatedCollectionClass` or `$_cursorPaginatedCollectionClass` properties +then take a look at the magic collect functionality on information about how to replace these. + +If you want to keep the old behaviour, you can use the `WithDeprecatedCollectionMethod` trait and `DeprecatedData` +interface on your data objects which adds the collection method again, this trait will probably be removed in v5. + +**Transformers (Likelihood Of Impact: High)** + +If you've implemented a custom transformer, then add the new `TransformationContext` parameter to the `transform` method +signature. + +```php +// v3 +public function transform(DataProperty $property, mixed $value): mixed +{ + // ... +} + +// v4 +public function transform(DataProperty $property,mixed $value,TransformationContext $context): mixed +{ + // ... +} +``` + +**Casts (Likelihood Of Impact: High)** + +If you've implemented a custom cast, then add the new $context `CreationContext` parameter to the `cast` method +signature and rename the old $context parameter to $properties. + +```php +// v3 +public function cast(DataProperty $property, mixed $value, array $context): mixed; +{ + // ... +} + +// v4 +public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed +{ + // ... +} +``` + +**The transform method (Likelihood Of Impact: Medium)** + +The transform method singature was changed to use a factory pattern instead of paramaters: + +```php +// v3 +$data->transform( + transformValues:true, + wrapExecutionType:WrapExecutionType::Disabled, + mapPropertyNames:true, +); + +// v4 +$data->transform( + TransformationContextFactory::create() + ->transformValues() + ->wrapExecutionType(WrapExecutionType::Disabled) + ->mapPropertyNames() +); +``` + +**Data Pipes (Likelihood Of Impact: Medium)** + +If you've implemented a custom data pipe, then add the new `CreationContext` parameter to the `handle` method +signature and change the type of the $properties parameter from `Collection` to `array`. Lastly, return an `array` of +properties instead of a `Collection`. + +```php +// v3 +public function handle(mixed $payload, DataClass $class, Collection $properties): Collection +{ + // ... +} + +// v4 +public function handle(mixed $payload, DataClass $class, array $properties, CreationContext $creationContext): array +{ + // ... +} + +``` + +**Invalid partials (Likelihood Of Impact: Medium)** + +Previously when trying to include, exclude, only or except certain data properties that did not exist the package +continued working. From now on an exception will be thrown, these exceptions can be silenced by +setting `ignore_invalid_partials` to `true` within the config file + +**Internal data structure changes (Likelihood Of Impact: Low)** + +If you use internal data structures like `DataClass` and `DataProperty` then take a look at these classes, a lot as +changed and the creation process now uses factories instead of static constructors. + +**Data interfaces and traits (Likelihood Of Impact: Low)** + +We've split up some interfaces and traits, if you were manually using these on your own data implementation then take a +look what has changed: + +- DataTrait was removed, you can easily build it yourself if required +- PrepareableData (interface and trait) were merged with BaseData +- An EmptyData (interface and trait) was added +- An ContextableData (interface and trait) was added + +**ValidatePropertiesDataPipe (Likelihood Of Impact: Low)** + +If you've used the `ValidatePropertiesDataPipe::allTypes` parameter to validate all types, then please use the new +context when creating a data object to enable this or update your `data.php` config file with the new default. + +**Removal of `withoutMagicalCreationFrom` (Likelihood Of Impact: Low)** + +If you used the `withoutMagicalCreationFrom` method on a data object, the now use the context to disable magical data +object creation: + +```php +// v3 +SomeData::withoutMagicalCreationFrom($payload); + +// v4 +SomeData::factory()->withoutMagicalCreation()->from($payload); +``` + +**Partials (Likelihood Of Impact: Low)** + +If you we're manually setting the `$_includes`, ` $_excludes`, `$_only`, `$_except` or `$_wrap` properties on a data +object, these have now been removed. Instead, you should use the new DataContext and add the partial. + +**Removal of `DataCollectableTransformer` and `DataTransformer` (Likelihood Of Impact: Low)** + +If you were using the `DataCollectableTransformer` or `DataTransformer` then please use the `TransformedDataCollectableResolver` and `TransformedDataResolver` instead. + +**Some advice with this new version of laravel-data** We advise you to take a look at the following things: -- Take a look within your data objects if `DataCollection`'s, `DataPaginatedCollection`'s and `DataCursorPaginatedCollection`'s can be replaced with regular arrays, Laravel Collections and Paginator + +- If you've cached data structures, be sure to clear the cache +- Take a look within your data objects if `DataCollection`'s, `DataPaginatedCollection`'s + and `DataCursorPaginatedCollection`'s can be replaced with regular arrays, Laravel Collections and Paginators making code a lot more readable - Replace `DataCollectionOf` attributes with annotations, providing IDE completion and more info for static analyzers - Replace some `extends Data` definitions with `extends Resource` or `extends Dto` for more minimal data objects -- When using `only` and `except` at the same time on a data object/collection, previously only the except would be executed. From now on, we first execute the except and then the only. +- When using `only` and `except` at the same time on a data object/collection, previously only the except would be + executed. From now on, we first execute the except and then the only. + ## From v2 to v3 -Upgrading to laravel data shouldn't take long, we've documented all possible changes just to provide the whole context. You probably won't have to do anything: +Upgrading to laravel data shouldn't take long, we've documented all possible changes just to provide the whole context. +You probably won't have to do anything: - Laravel 9 is now required -- validation is completely rewritten - - rules are now generated based upon the payload provided, not what a payload possibly could be. This means rules can change depending on the provided payload. - - When you're injecting a `$payload`, `$relativePayload` or `$path` parameter in a custom rules method in your data object, then remove this and use the new `ValidationContext`: +- validation is completely rewritten + - rules are now generated based upon the payload provided, not what a payload possibly could be. This means rules + can change depending on the provided payload. + - When you're injecting a `$payload`, `$relativePayload` or `$path` parameter in a custom rules method in your data + object, then remove this and use the new `ValidationContext`: ```php class SomeData extends Data { @@ -62,14 +203,19 @@ class SomeData extends Data { } } ``` - - The type of `$rules` in the RuleInferrer handle method changed from `RulesCollection` to `PropertyRules` - - RuleInferrers now take a $context parameter which is a `ValidationContext` in their handle method - - Validation attributes now keep track where they are being evaluated when you have nested data objects. Now field references are relative to the object and not to the root validated object - - Some resolvers are removed like: `DataClassValidationRulesResolver`, `DataPropertyValidationRulesResolver` - - The default order of rule inferrers has been changed - - The $payload parameter in the `getValidationRules` method is now required - - The $fields parameter was removed from the `getValidationRules` method, this now should be done outside of the package -- all data specific properties are now prefixed with _, to avoid conflicts with properties with your own defined properties. This is especially important when overwriting `$collectionClass`, `$paginatedCollectionClass`, `$cursorPaginatedCollectionClass`, be sure to add the extra _ within your data classes. + +- The type of `$rules` in the RuleInferrer handle method changed from `RulesCollection` to `PropertyRules` +- RuleInferrers now take a $context parameter which is a `ValidationContext` in their handle method +- Validation attributes now keep track where they are being evaluated when you have nested data objects. Now field + references are relative to the object and not to the root validated object +- Some resolvers are removed like: `DataClassValidationRulesResolver`, `DataPropertyValidationRulesResolver` +- The default order of rule inferrers has been changed +- The $payload parameter in the `getValidationRules` method is now required +- The $fields parameter was removed from the `getValidationRules` method, this now should be done outside of the package +- all data specific properties are now prefixed with _, to avoid conflicts with properties with your own defined + properties. This is especially important when + overwriting `$collectionClass`, `$paginatedCollectionClass`, `$cursorPaginatedCollectionClass`, be sure to add the + extra _ within your data classes. - Serialization logic is now updated and will only include data specific properties ## From v1 to v2 @@ -79,7 +225,9 @@ High impact changes - Please check the most recent `data.php` config file and change yours accordingly - The `Cast` interface now has a `$context` argument in the `cast` method - The `RuleInferrer` interface now has a `RulesCollection` argument instead of an `array` -- By default, it is now impossible to include/exclude properties using a request parameter until manually specified. This behaviour can be overwritten by adding these methods to your data object: +- By default, it is now impossible to include/exclude properties using a request parameter until manually specified. + This behaviour can be overwritten by adding these methods to your data object: + ```php public static function allowedRequestIncludes(): ?array { @@ -91,8 +239,10 @@ High impact changes return null; } ``` + - The `validate` method on a data object will not create a data object after validation, use `validateAndCreate` instead -- `DataCollection` is now being split into a `DataCollection`, `PaginatedDataCollection` and `CursorPaginatedDataCollection` +- `DataCollection` is now being split into a `DataCollection`, `PaginatedDataCollection` + and `CursorPaginatedDataCollection` Low impact changes @@ -105,4 +255,5 @@ Low impact changes - The `DataTypeScriptTransformer` is updated for this new version, if you extend this then please take a look - The `DataTransformer` and `DataCollectionTransformer` now use a `WrapExecutionType` - The `filter` method was removed from paginated data collections -- The `through` and `filter` operations on a `DataCollection` will now happen instant instead of waiting for the transforming process +- The `through` and `filter` operations on a `DataCollection` will now happen instant instead of waiting for the + transforming process diff --git a/src/Casts/Cast.php b/src/Casts/Cast.php index fa87837d..fc2dbf59 100644 --- a/src/Casts/Cast.php +++ b/src/Casts/Cast.php @@ -8,5 +8,5 @@ interface Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed; + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed; } diff --git a/src/Casts/DateTimeInterfaceCast.php b/src/Casts/DateTimeInterfaceCast.php index 6a4bdf49..0dd44dac 100644 --- a/src/Casts/DateTimeInterfaceCast.php +++ b/src/Casts/DateTimeInterfaceCast.php @@ -4,7 +4,6 @@ use DateTimeInterface; use DateTimeZone; -use Illuminate\Support\Collection; use Spatie\LaravelData\Exceptions\CannotCastDate; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; @@ -19,7 +18,7 @@ public function __construct( ) { } - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): DateTimeInterface|Uncastable + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): DateTimeInterface|Uncastable { $formats = collect($this->format ?? config('data.date_format')); diff --git a/src/Casts/EnumCast.php b/src/Casts/EnumCast.php index 996fc804..f305d979 100644 --- a/src/Casts/EnumCast.php +++ b/src/Casts/EnumCast.php @@ -16,7 +16,7 @@ public function __construct( ) { } - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): BackedEnum | Uncastable + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): BackedEnum | Uncastable { $type = $this->type ?? $property->type->type->findAcceptedTypeForBaseType(BackedEnum::class); diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 3c7b95fc..53b4d618 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -80,7 +80,7 @@ public static function pipeline(): DataPipeline ->through(CastPropertiesDataPipe::class); } - public static function prepareForPipeline(Collection $properties): Collection + public static function prepareForPipeline(array $properties): array { return $properties; } diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index c9dfdb76..09ffe7c6 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -24,7 +24,7 @@ public function transform( $resolver = match (true) { $this instanceof BaseDataContract => DataContainer::get()->transformedDataResolver(), - $this instanceof BaseDataCollectableContract => DataContainer::get()->transformedDataCollectionResolver(), + $this instanceof BaseDataCollectableContract => DataContainer::get()->transformedDataCollectableResolver(), default => throw new Exception('Cannot transform data object') }; diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index bd316446..3675e550 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -48,7 +48,7 @@ public static function factory(?CreationContext $creationContext = null): Creati public static function normalizers(): array; - public static function prepareForPipeline(Collection $properties): Collection; + public static function prepareForPipeline(array $properties): array; public static function pipeline(): DataPipeline; diff --git a/src/DataPipes/AuthorizedDataPipe.php b/src/DataPipes/AuthorizedDataPipe.php index c044a992..84377549 100644 --- a/src/DataPipes/AuthorizedDataPipe.php +++ b/src/DataPipes/AuthorizedDataPipe.php @@ -13,9 +13,9 @@ class AuthorizedDataPipe implements DataPipe public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection { + ): array { if (! $payload instanceof Request) { return $properties; } diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 36edd69a..459c6296 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -20,9 +20,9 @@ public function __construct( public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection { + ): array { foreach ($properties as $name => $value) { $dataProperty = $class->properties->first(fn (DataProperty $dataProperty) => $dataProperty->name === $name); @@ -43,7 +43,7 @@ public function handle( protected function cast( DataProperty $property, mixed $value, - Collection $properties, + array $properties, CreationContext $creationContext ): mixed { $shouldCast = $this->shouldBeCasted($property, $value); diff --git a/src/DataPipes/DataPipe.php b/src/DataPipes/DataPipe.php index 59bae51b..69f24982 100644 --- a/src/DataPipes/DataPipe.php +++ b/src/DataPipes/DataPipe.php @@ -11,15 +11,15 @@ interface DataPipe /** * @param mixed $payload * @param DataClass $class - * @param Collection $properties + * @param array $properties * @param CreationContext $creationContext * - * @return Collection + * @return array */ public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection; + ): array; } diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index 56ff601c..8d37acbb 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -12,11 +12,11 @@ class DefaultValuesDataPipe implements DataPipe public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection { + ): array { foreach ($class->properties as $name => $property) { - if($properties->has($name)) { + if(array_key_exists($name, $properties)) { continue; } diff --git a/src/DataPipes/FillRouteParameterPropertiesDataPipe.php b/src/DataPipes/FillRouteParameterPropertiesDataPipe.php index ca667e8f..52270ed5 100644 --- a/src/DataPipes/FillRouteParameterPropertiesDataPipe.php +++ b/src/DataPipes/FillRouteParameterPropertiesDataPipe.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\DataPipes; use Illuminate\Http\Request; -use Illuminate\Support\Collection; use Spatie\LaravelData\Attributes\FromRouteParameter; use Spatie\LaravelData\Attributes\FromRouteParameterProperty; use Spatie\LaravelData\Exceptions\CannotFillFromRouteParameterPropertyUsingScalarValue; @@ -16,9 +15,9 @@ class FillRouteParameterPropertiesDataPipe implements DataPipe public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection { + ): array { if (! $payload instanceof Request) { return $properties; } @@ -32,7 +31,7 @@ public function handle( continue; } - if (! $attribute->replaceWhenPresentInBody && $properties->has($dataProperty->name)) { + if (! $attribute->replaceWhenPresentInBody && array_key_exists($dataProperty->name, $properties)) { continue; } @@ -42,10 +41,7 @@ public function handle( continue; } - $properties->put( - $dataProperty->name, - $this->resolveValue($dataProperty, $attribute, $parameter) - ); + $properties[$dataProperty->name] = $this->resolveValue($dataProperty, $attribute, $parameter); } return $properties; diff --git a/src/DataPipes/MapPropertiesDataPipe.php b/src/DataPipes/MapPropertiesDataPipe.php index 90a5a120..b8681df3 100644 --- a/src/DataPipes/MapPropertiesDataPipe.php +++ b/src/DataPipes/MapPropertiesDataPipe.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\DataPipes; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; @@ -12,9 +11,9 @@ class MapPropertiesDataPipe implements DataPipe public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection { + ): array { if ($creationContext->mapPropertyNames === false) { return $properties; } @@ -25,7 +24,7 @@ public function handle( } if (Arr::has($properties, $dataProperty->inputMappedName)) { - $properties->put($dataProperty->name, Arr::get($properties, $dataProperty->inputMappedName)); + $properties[$dataProperty->name] = Arr::get($properties, $dataProperty->inputMappedName); } } diff --git a/src/DataPipes/ValidatePropertiesDataPipe.php b/src/DataPipes/ValidatePropertiesDataPipe.php index cbbc25bd..34b6884e 100644 --- a/src/DataPipes/ValidatePropertiesDataPipe.php +++ b/src/DataPipes/ValidatePropertiesDataPipe.php @@ -13,9 +13,9 @@ class ValidatePropertiesDataPipe implements DataPipe public function handle( mixed $payload, DataClass $class, - Collection $properties, + array $properties, CreationContext $creationContext - ): Collection { + ): array { if ($creationContext->validationType === ValidationType::Disabled) { return $properties; } diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 45ae6207..27341cef 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -26,47 +26,48 @@ public function __construct(protected DataConfig $dataConfig) * * @return TData */ - public function execute(string $class, Collection $properties): BaseData + public function execute(string $class, array $properties): BaseData { $dataClass = $this->dataConfig->getDataClass($class); $data = $this->createData($dataClass, $properties); - $dataClass - ->properties - ->reject( - fn (DataProperty $property) => $property->isPromoted - || $property->isReadonly - || ! $properties->has($property->name) - ) - ->each(function (DataProperty $property) use ($properties, $data) { - if ($property->type->isOptional - && isset($data->{$property->name}) - && $properties->get($property->name) instanceof Optional - ) { - return; - } - - if ($property->computed - && $property->type->isNullable - && $properties->get($property->name) === null - ) { - return; // Nullable properties get assigned null by default - } - - if ($property->computed) { - throw CannotSetComputedValue::create($property); - } - - $data->{$property->name} = $properties->get($property->name); - }); + foreach ($dataClass->properties as $property) { + if( + $property->isPromoted + || $property->isReadonly + || ! array_key_exists($property->name, $properties) + ){ + continue; + } + + if ($property->type->isOptional + && isset($data->{$property->name}) + && $properties[$property->name] instanceof Optional + ) { + continue; + } + + if ($property->computed + && $property->type->isNullable + && $properties[$property->name] === null + ) { + continue; // Nullable properties get assigned null by default + } + + if ($property->computed) { + throw CannotSetComputedValue::create($property); + } + + $data->{$property->name} = $properties[$property->name]; + } return $data; } protected function createData( DataClass $dataClass, - Collection $properties, + array $properties, ) { $constructorParameters = $dataClass->constructorMethod?->parameters; @@ -77,8 +78,8 @@ protected function createData( $parameters = []; foreach ($constructorParameters as $parameter) { - if ($properties->has($parameter->name)) { - $parameters[$parameter->name] = $properties->get($parameter->name); + if (array_key_exists($parameter->name, $properties)) { + $parameters[$parameter->name] = $properties[$parameter->name]; continue; } diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index e13eeb29..b7c23a76 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -34,7 +34,7 @@ public function execute( return $data; } - $properties = new Collection(); + $properties = []; $pipeline = $this->dataConfig->getResolvedDataPipeline($class); diff --git a/src/Resolvers/TransformedDataCollectionResolver.php b/src/Resolvers/TransformedDataCollectableResolver.php similarity index 98% rename from src/Resolvers/TransformedDataCollectionResolver.php rename to src/Resolvers/TransformedDataCollectableResolver.php index 5a62bae0..bb25233f 100644 --- a/src/Resolvers/TransformedDataCollectionResolver.php +++ b/src/Resolvers/TransformedDataCollectableResolver.php @@ -20,7 +20,7 @@ use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Support\Wrapping\WrapType; -class TransformedDataCollectionResolver +class TransformedDataCollectableResolver { public function __construct( protected DataConfig $dataConfig diff --git a/src/Resolvers/TransformedDataResolver.php b/src/Resolvers/TransformedDataResolver.php index 3722b7c3..e3447c98 100644 --- a/src/Resolvers/TransformedDataResolver.php +++ b/src/Resolvers/TransformedDataResolver.php @@ -161,7 +161,7 @@ protected function resolvePropertyValue( WrapExecutionType::TemporarilyDisabled => WrapExecutionType::Enabled }; - return DataContainer::get()->transformedDataCollectionResolver()->execute( + return DataContainer::get()->transformedDataCollectableResolver()->execute( $value, $fieldContext->setWrapExecutionType($wrapExecutionType) ); diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index 3f27d3e2..0644d03b 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -5,7 +5,7 @@ use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; -use Spatie\LaravelData\Resolvers\TransformedDataCollectionResolver; +use Spatie\LaravelData\Resolvers\TransformedDataCollectableResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; use Spatie\LaravelData\Support\Factories\DataClassFactory; @@ -15,7 +15,7 @@ class DataContainer protected ?TransformedDataResolver $transformedDataResolver = null; - protected ?TransformedDataCollectionResolver $transformedDataCollectionResolver = null; + protected ?TransformedDataCollectableResolver $transformedDataCollectableResolver = null; protected ?RequestQueryStringPartialsResolver $requestQueryStringPartialsResolver = null; @@ -43,9 +43,9 @@ public function transformedDataResolver(): TransformedDataResolver return $this->transformedDataResolver ??= app(TransformedDataResolver::class); } - public function transformedDataCollectionResolver(): TransformedDataCollectionResolver + public function transformedDataCollectableResolver(): TransformedDataCollectableResolver { - return $this->transformedDataCollectionResolver ??= app(TransformedDataCollectionResolver::class); + return $this->transformedDataCollectableResolver ??= app(TransformedDataCollectableResolver::class); } public function requestQueryStringPartialsResolver(): RequestQueryStringPartialsResolver @@ -71,7 +71,7 @@ public function dataClassFactory(): DataClassFactory public function reset() { $this->transformedDataResolver = null; - $this->transformedDataCollectionResolver = null; + $this->transformedDataCollectableResolver = null; $this->requestQueryStringPartialsResolver = null; $this->dataFromSomethingResolver = null; $this->dataCollectableFromSomethingResolver = null; diff --git a/src/Support/ResolvedDataPipeline.php b/src/Support/ResolvedDataPipeline.php index 4ae2cf55..9bbfa2da 100644 --- a/src/Support/ResolvedDataPipeline.php +++ b/src/Support/ResolvedDataPipeline.php @@ -19,7 +19,7 @@ public function __construct( ) { } - public function execute(mixed $value, CreationContext $creationContext): Collection + public function execute(mixed $value, CreationContext $creationContext): array { $properties = null; @@ -35,8 +35,6 @@ public function execute(mixed $value, CreationContext $creationContext): Collect throw CannotCreateData::noNormalizerFound($this->dataClass->name, $value); } - $properties = collect($properties); - $properties = ($this->dataClass->name)::prepareForPipeline($properties); foreach ($this->pipes as $pipe) { diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index d7508618..2742f157 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -28,7 +28,7 @@ $caster->cast( FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new Carbon('19-05-1994 00:00:00')); @@ -37,7 +37,7 @@ $caster->cast( FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new CarbonImmutable('19-05-1994 00:00:00')); @@ -46,7 +46,7 @@ $caster->cast( FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new DateTime('19-05-1994 00:00:00')); @@ -55,7 +55,7 @@ $caster->cast( FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new DateTimeImmutable('19-05-1994 00:00:00')); @@ -72,7 +72,7 @@ $caster->cast( FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(new DateTime('19-05-1994 00:00:00')); @@ -89,7 +89,7 @@ $caster->cast( FakeDataStructureFactory::property($class, 'int'), '1994-05-16 12:20:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(Uncastable::create()); @@ -111,7 +111,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') @@ -120,7 +120,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') @@ -129,7 +129,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') @@ -138,7 +138,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') @@ -161,7 +161,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'carbon'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') @@ -170,7 +170,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'carbonImmutable'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') @@ -179,7 +179,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'dateTime'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') @@ -188,7 +188,7 @@ expect($caster->cast( FakeDataStructureFactory::property($class, 'dateTimeImmutable'), '19-05-1994 00:00:00', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() )) ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') diff --git a/tests/Casts/EnumCastTest.php b/tests/Casts/EnumCastTest.php index 791ba8d3..219fca57 100644 --- a/tests/Casts/EnumCastTest.php +++ b/tests/Casts/EnumCastTest.php @@ -20,7 +20,7 @@ $this->caster->cast( FakeDataStructureFactory::property($class, 'enum'), 'foo', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(DummyBackedEnum::FOO); @@ -35,7 +35,7 @@ $this->caster->cast( FakeDataStructureFactory::property($class, 'enum'), 'bar', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(DummyBackedEnum::FOO); @@ -50,7 +50,7 @@ $this->caster->cast( FakeDataStructureFactory::property($class, 'enum'), 'foo', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get() ) )->toEqual(Uncastable::create()); @@ -65,7 +65,7 @@ $this->caster->cast( FakeDataStructureFactory::property($class, 'int'), 'foo', - collect(), + [], CreationContextFactory::createFromConfig($class::class)->get(), ) ) diff --git a/tests/Fakes/CastTransformers/FakeCastTransformer.php b/tests/Fakes/CastTransformers/FakeCastTransformer.php index 1fc80e23..6855b2f3 100644 --- a/tests/Fakes/CastTransformers/FakeCastTransformer.php +++ b/tests/Fakes/CastTransformers/FakeCastTransformer.php @@ -11,7 +11,7 @@ class FakeCastTransformer implements Cast, Transformer { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed { return $value; } diff --git a/tests/Fakes/Castables/SimpleCastable.php b/tests/Fakes/Castables/SimpleCastable.php index 4124c6f2..416e605d 100644 --- a/tests/Fakes/Castables/SimpleCastable.php +++ b/tests/Fakes/Castables/SimpleCastable.php @@ -17,7 +17,7 @@ public function __construct(public string $value) public static function dataCastUsing(...$arguments): Cast { return new class () implements Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed { return new SimpleCastable($value); } diff --git a/tests/Fakes/Casts/ConfidentialDataCast.php b/tests/Fakes/Casts/ConfidentialDataCast.php index c551ee66..e3a9612c 100644 --- a/tests/Fakes/Casts/ConfidentialDataCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCast.php @@ -10,7 +10,7 @@ class ConfidentialDataCast implements Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): SimpleData + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): SimpleData { return SimpleData::from('CONFIDENTIAL'); } diff --git a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php index d991ab2a..309787df 100644 --- a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php @@ -10,7 +10,7 @@ class ConfidentialDataCollectionCast implements Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): array + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): array { return array_map(fn () => SimpleData::from('CONFIDENTIAL'), $value); } diff --git a/tests/Fakes/Casts/ContextAwareCast.php b/tests/Fakes/Casts/ContextAwareCast.php index 6d1976d3..65ff1afc 100644 --- a/tests/Fakes/Casts/ContextAwareCast.php +++ b/tests/Fakes/Casts/ContextAwareCast.php @@ -9,8 +9,8 @@ class ContextAwareCast implements Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): mixed + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed { - return $value . '+' . $properties->toJson(); + return $value . '+' . json_encode($properties); } } diff --git a/tests/Fakes/Casts/MeaningOfLifeCast.php b/tests/Fakes/Casts/MeaningOfLifeCast.php index 598cc87e..e473e308 100644 --- a/tests/Fakes/Casts/MeaningOfLifeCast.php +++ b/tests/Fakes/Casts/MeaningOfLifeCast.php @@ -9,7 +9,7 @@ class MeaningOfLifeCast implements Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): int + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): int { return 42; } diff --git a/tests/Fakes/Casts/StringToUpperCast.php b/tests/Fakes/Casts/StringToUpperCast.php index 3e2fac6a..ca518461 100644 --- a/tests/Fakes/Casts/StringToUpperCast.php +++ b/tests/Fakes/Casts/StringToUpperCast.php @@ -9,7 +9,7 @@ class StringToUpperCast implements Cast { - public function cast(DataProperty $property, mixed $value, Collection $properties, CreationContext $context): string + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): string { return strtoupper($value); } diff --git a/tests/PipelineTest.php b/tests/PipelineTest.php index 79ba038a..caa5efb1 100644 --- a/tests/PipelineTest.php +++ b/tests/PipelineTest.php @@ -1,5 +1,6 @@ put('address', $properties->only(['line_1', 'city', 'state', 'zipcode'])->join(',')); + $properties['address'] = implode(',', Arr::only($properties, ['line_1', 'city', 'state', 'zipcode'])); return $properties; } diff --git a/tests/TestSupport/DataValidationAsserter.php b/tests/TestSupport/DataValidationAsserter.php index 1e8a5df4..ac33fa4f 100644 --- a/tests/TestSupport/DataValidationAsserter.php +++ b/tests/TestSupport/DataValidationAsserter.php @@ -167,6 +167,6 @@ private function pipePayload(array $payload): array ->resolve() ->execute($payload, CreationContextFactory::createFromConfig($this->dataClass)->get()); - return $properties->all(); + return $properties; } } From 3e53091955de8c3003144a3acf51ac886ad1769c Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Fri, 19 Jan 2024 14:49:00 +0000 Subject: [PATCH 095/124] Fix styling --- src/Casts/Cast.php | 1 - src/Casts/EnumCast.php | 1 - src/DataPipes/AuthorizedDataPipe.php | 1 - src/DataPipes/CastPropertiesDataPipe.php | 1 - src/DataPipes/DataPipe.php | 1 - src/DataPipes/DefaultValuesDataPipe.php | 1 - src/DataPipes/ValidatePropertiesDataPipe.php | 1 - src/Resolvers/DataFromArrayResolver.php | 4 +--- src/Resolvers/DataFromSomethingResolver.php | 1 - src/Support/ResolvedDataPipeline.php | 1 - tests/Fakes/CastTransformers/FakeCastTransformer.php | 1 - tests/Fakes/Castables/SimpleCastable.php | 1 - tests/Fakes/Casts/ConfidentialDataCast.php | 1 - tests/Fakes/Casts/ConfidentialDataCollectionCast.php | 1 - tests/Fakes/Casts/ContextAwareCast.php | 1 - tests/Fakes/Casts/MeaningOfLifeCast.php | 1 - tests/Fakes/Casts/StringToUpperCast.php | 1 - tests/PipelineTest.php | 1 - 18 files changed, 1 insertion(+), 20 deletions(-) diff --git a/src/Casts/Cast.php b/src/Casts/Cast.php index fc2dbf59..e3ba2446 100644 --- a/src/Casts/Cast.php +++ b/src/Casts/Cast.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Casts; -use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/src/Casts/EnumCast.php b/src/Casts/EnumCast.php index f305d979..2c8a78c0 100644 --- a/src/Casts/EnumCast.php +++ b/src/Casts/EnumCast.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Casts; use BackedEnum; -use Illuminate\Support\Collection; use Spatie\LaravelData\Exceptions\CannotCastEnum; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/src/DataPipes/AuthorizedDataPipe.php b/src/DataPipes/AuthorizedDataPipe.php index 84377549..14b4d018 100644 --- a/src/DataPipes/AuthorizedDataPipe.php +++ b/src/DataPipes/AuthorizedDataPipe.php @@ -4,7 +4,6 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Http\Request; -use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 459c6296..599f127c 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\DataPipes; -use Illuminate\Support\Collection; use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Creation\CreationContext; diff --git a/src/DataPipes/DataPipe.php b/src/DataPipes/DataPipe.php index 69f24982..ceb7f64f 100644 --- a/src/DataPipes/DataPipe.php +++ b/src/DataPipes/DataPipe.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\DataPipes; -use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index 8d37acbb..0a3964f6 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\DataPipes; -use Illuminate\Support\Collection; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; diff --git a/src/DataPipes/ValidatePropertiesDataPipe.php b/src/DataPipes/ValidatePropertiesDataPipe.php index 34b6884e..b1baa4a0 100644 --- a/src/DataPipes/ValidatePropertiesDataPipe.php +++ b/src/DataPipes/ValidatePropertiesDataPipe.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\DataPipes; use Illuminate\Http\Request; -use Illuminate\Support\Collection; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\ValidationType; use Spatie\LaravelData\Support\DataClass; diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 27341cef..85cec2c4 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -3,14 +3,12 @@ namespace Spatie\LaravelData\Resolvers; use ArgumentCountError; -use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Exceptions\CannotCreateData; use Spatie\LaravelData\Exceptions\CannotSetComputedValue; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; -use Spatie\LaravelData\Support\DataProperty; /** * @template TData of BaseData @@ -37,7 +35,7 @@ public function execute(string $class, array $properties): BaseData $property->isPromoted || $property->isReadonly || ! array_key_exists($property->name, $properties) - ){ + ) { continue; } diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index b7c23a76..17d0d9aa 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Resolvers; use Illuminate\Http\Request; -use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Enums\CustomCreationMethodType; use Spatie\LaravelData\Support\Creation\CreationContext; diff --git a/src/Support/ResolvedDataPipeline.php b/src/Support/ResolvedDataPipeline.php index 9bbfa2da..5fb14d9d 100644 --- a/src/Support/ResolvedDataPipeline.php +++ b/src/Support/ResolvedDataPipeline.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Support; -use Illuminate\Support\Collection; use Spatie\LaravelData\Exceptions\CannotCreateData; use Spatie\LaravelData\Support\Creation\CreationContext; diff --git a/tests/Fakes/CastTransformers/FakeCastTransformer.php b/tests/Fakes/CastTransformers/FakeCastTransformer.php index 6855b2f3..a90f19f2 100644 --- a/tests/Fakes/CastTransformers/FakeCastTransformer.php +++ b/tests/Fakes/CastTransformers/FakeCastTransformer.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\CastTransformers; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/tests/Fakes/Castables/SimpleCastable.php b/tests/Fakes/Castables/SimpleCastable.php index 416e605d..115e6e0e 100644 --- a/tests/Fakes/Castables/SimpleCastable.php +++ b/tests/Fakes/Castables/SimpleCastable.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Castables; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Casts\Castable; use Spatie\LaravelData\Support\Creation\CreationContext; diff --git a/tests/Fakes/Casts/ConfidentialDataCast.php b/tests/Fakes/Casts/ConfidentialDataCast.php index e3a9612c..b4b81dfa 100644 --- a/tests/Fakes/Casts/ConfidentialDataCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCast.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php index 309787df..d66aca1a 100644 --- a/tests/Fakes/Casts/ConfidentialDataCollectionCast.php +++ b/tests/Fakes/Casts/ConfidentialDataCollectionCast.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/tests/Fakes/Casts/ContextAwareCast.php b/tests/Fakes/Casts/ContextAwareCast.php index 65ff1afc..ac60ae16 100644 --- a/tests/Fakes/Casts/ContextAwareCast.php +++ b/tests/Fakes/Casts/ContextAwareCast.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/tests/Fakes/Casts/MeaningOfLifeCast.php b/tests/Fakes/Casts/MeaningOfLifeCast.php index e473e308..22de578f 100644 --- a/tests/Fakes/Casts/MeaningOfLifeCast.php +++ b/tests/Fakes/Casts/MeaningOfLifeCast.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/tests/Fakes/Casts/StringToUpperCast.php b/tests/Fakes/Casts/StringToUpperCast.php index ca518461..613177ef 100644 --- a/tests/Fakes/Casts/StringToUpperCast.php +++ b/tests/Fakes/Casts/StringToUpperCast.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Tests\Fakes\Casts; -use Illuminate\Support\Collection; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; diff --git a/tests/PipelineTest.php b/tests/PipelineTest.php index caa5efb1..cd7d5c4e 100644 --- a/tests/PipelineTest.php +++ b/tests/PipelineTest.php @@ -1,7 +1,6 @@ Date: Fri, 19 Jan 2024 15:52:55 +0100 Subject: [PATCH 096/124] wip --- benchmarks/DataBench.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/benchmarks/DataBench.php b/benchmarks/DataBench.php index fd956c68..792f6726 100644 --- a/benchmarks/DataBench.php +++ b/benchmarks/DataBench.php @@ -183,7 +183,7 @@ public function setupObjectCreation() Revs(500), Iterations(5), BeforeMethods(['setupCache', 'setupCollectionTransformation']), - Assert('mode(variant.time.avg) < 580 microseconds +/- 5%') + Assert('mode(variant.time.avg) < 590 microseconds +/- 5%') ] public function benchCollectionTransformation() { @@ -194,7 +194,7 @@ public function benchCollectionTransformation() Revs(5000), Iterations(5), BeforeMethods(['setupCache', 'setupObjectTransformation']), - Assert('mode(variant.time.avg) < 38 microseconds +/- 5%') + Assert('mode(variant.time.avg) < 39 microseconds +/- 5%') ] public function benchObjectTransformation() { @@ -205,7 +205,7 @@ public function benchObjectTransformation() Revs(500), Iterations(5), BeforeMethods(['setupCache', 'setupCollectionCreation']), - Assert('mode(variant.time.avg) < 1.86 milliseconds +/- 5%') + Assert('mode(variant.time.avg) < 1.335 milliseconds +/- 5%') ] public function benchCollectionCreation() { @@ -216,7 +216,7 @@ public function benchCollectionCreation() Revs(5000), Iterations(5), BeforeMethods(['setupCache', 'setupObjectCreation']), - Assert('mode(variant.time.avg) < 129 microseconds +/- 5%') + Assert('mode(variant.time.avg) < 90 microseconds +/- 5%') ] public function benchObjectCreation() { @@ -227,7 +227,7 @@ public function benchObjectCreation() Revs(500), Iterations(5), BeforeMethods(['setupCollectionTransformation']), - Assert('mode(variant.time.avg) < 774 microseconds +/- 10%') + Assert('mode(variant.time.avg) < 791 microseconds +/- 10%') ] public function benchCollectionTransformationWithoutCache() { @@ -240,7 +240,7 @@ public function benchCollectionTransformationWithoutCache() Revs(5000), Iterations(5), BeforeMethods(['setupObjectTransformation']), - Assert('mode(variant.time.avg) < 217 microseconds +/- 10%') + Assert('mode(variant.time.avg) < 226 microseconds +/- 10%') ] public function benchObjectTransformationWithoutCache() { @@ -253,7 +253,7 @@ public function benchObjectTransformationWithoutCache() Revs(500), Iterations(5), BeforeMethods(['setupCollectionCreation']), - Assert('mode(variant.time.avg) < 2.15 milliseconds +/- 10%') + Assert('mode(variant.time.avg) < 1.62 milliseconds +/- 10%') ] public function benchCollectionCreationWithoutCache() { @@ -266,7 +266,7 @@ public function benchCollectionCreationWithoutCache() Revs(5000), Iterations(5), BeforeMethods(['setupObjectCreation']), - Assert('mode(variant.time.avg) < 367 microseconds +/- 10%') + Assert('mode(variant.time.avg) < 347 microseconds +/- 10%') ] public function benchObjectCreationWithoutCache() { From b26206a4fc106b3f3c776e4b5b37736e2a890786 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 19 Jan 2024 16:38:23 +0100 Subject: [PATCH 097/124] wip --- CHANGELOG.md | 6 ++++++ src/Casts/Castable.php | 4 +--- src/Support/DataMethod.php | 32 ++++++++++++++++++++++---------- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a132ba..dc270f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ All notable changes to `laravel-data` will be documented in this file. - Added contexts to the creation and transformation process - Allow creating a data object or collection using a factory - Speed up the process of creating and transforming data objects +- Rewritten docs + +**Some more "internal" changes** + +- Restructured tests for the future we have ahead +- Benchmarks added to make data even faster ## 3.11.0 - 2023-12-21 diff --git a/src/Casts/Castable.php b/src/Casts/Castable.php index ce560929..71f300d7 100644 --- a/src/Casts/Castable.php +++ b/src/Casts/Castable.php @@ -5,9 +5,7 @@ interface Castable { /** - * Get the name of the caster class to use when casting to this cast target. - * - * @param array $arguments + * @param array $arguments */ public static function dataCastUsing(array $arguments): Cast; } diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 52586f3c..19028a28 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -6,7 +6,7 @@ use Spatie\LaravelData\Enums\CustomCreationMethodType; /** - * @property Collection $parameters + * @property Collection $parameters */ class DataMethod { @@ -22,20 +22,31 @@ public function __construct( public function accepts(mixed ...$input): bool { - /** @var Collection $parameters */ - $parameters = array_is_list($input) - ? $this->parameters - : $this->parameters->mapWithKeys(fn (DataParameter|DataProperty $parameter) => [$parameter->name => $parameter]); + $requiredParameterCount = 0; - $parameters = $parameters->reject( - fn (DataParameter|DataProperty $parameter) => $parameter instanceof DataParameter && $parameter->type->type->isCreationContext() - ); + foreach ($this->parameters as $parameter) { + if ($parameter->type->type->isCreationContext()) { + continue; + } + + $requiredParameterCount++; + } - if (count($input) > $parameters->count()) { + if (count($input) > $requiredParameterCount) { return false; } - foreach ($parameters as $index => $parameter) { + $useNameAsIndex = ! array_is_list($input); + + foreach ($this->parameters as $index => $parameter) { + if ($parameter->type->type->isCreationContext()) { + continue; + } + + if ($useNameAsIndex) { + $index = $parameter->name; + } + $parameterProvided = array_key_exists($index, $input); if (! $parameterProvided && $parameter->hasDefaultValue === false) { @@ -59,6 +70,7 @@ public function accepts(mixed ...$input): bool ) { return false; } + } return true; From 8b19bb6eb57c4807be328dd0da5d8a2c96144c26 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 22 Jan 2024 12:27:15 +0100 Subject: [PATCH 098/124] wip --- docs/requirements.md | 2 +- src/DataCollection.php | 1 + src/Support/DataType.php | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/requirements.md b/docs/requirements.md index 64dfe63c..ab37db09 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -5,4 +5,4 @@ weight: 3 This package requires: - PHP 8.1 or higher -- Laravel 9 or higher +- Laravel 10 or higher diff --git a/src/DataCollection.php b/src/DataCollection.php index d4bf71d0..5e4fe370 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -5,6 +5,7 @@ use ArrayAccess; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; +use Illuminate\Support\LazyCollection; use Spatie\LaravelData\Concerns\BaseDataCollectable; use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\EnumerableMethods; diff --git a/src/Support/DataType.php b/src/Support/DataType.php index be41ef0e..b72c7ad1 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -6,9 +6,6 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Support\Types\Type; -/** - * @template T of Type - */ class DataType { /** From 174b6e5dc7e32fba59819b7f33a02482f7cff1f1 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 22 Jan 2024 14:13:36 +0100 Subject: [PATCH 099/124] Refactor types even more --- src/Enums/DataTypeKind.php | 8 +- .../DataCollectableFromSomethingResolver.php | 17 +- src/Support/DataMethod.php | 2 +- src/Support/DataProperty.php | 2 +- src/Support/DataPropertyType.php | 30 ++++ src/Support/DataReturnType.php | 20 --- src/Support/DataType.php | 16 +- src/Support/Factories/DataMethodFactory.php | 8 +- src/Support/Factories/DataPropertyFactory.php | 2 +- .../Factories/DataReturnTypeFactory.php | 74 ++++---- src/Support/Factories/DataTypeFactory.php | 164 ++++++++++++++---- .../DataTypeScriptTransformer.php | 6 +- .../Types/Storage/AcceptedTypesStorage.php | 8 +- tests/Factories/FakeDataStructureFactory.php | 8 - tests/Support/DataParameterTest.php | 11 ++ ...aTypeTest.php => DataPropertyTypeTest.php} | 66 ++++--- tests/Support/DataReturnTypeTest.php | 64 +++++-- 17 files changed, 341 insertions(+), 165 deletions(-) create mode 100644 src/Support/DataPropertyType.php delete mode 100644 src/Support/DataReturnType.php rename tests/Support/{DataTypeTest.php => DataPropertyTypeTest.php} (95%) diff --git a/src/Enums/DataTypeKind.php b/src/Enums/DataTypeKind.php index 553e1ab3..68f6f8da 100644 --- a/src/Enums/DataTypeKind.php +++ b/src/Enums/DataTypeKind.php @@ -9,10 +9,10 @@ enum DataTypeKind case DataCollection; case DataPaginatedCollection; case DataCursorPaginatedCollection; - case Array; - case Enumerable; - case Paginator; - case CursorPaginator; + case DataArray; + case DataEnumerable; + case DataPaginator; + case DataCursorPaginator; public function isDataObject(): bool { diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index d430d273..d98942e5 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -21,6 +21,7 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; +use Spatie\LaravelData\Support\Types\NamedType; class DataCollectableFromSomethingResolver { @@ -38,8 +39,8 @@ public function execute( ?string $into = null, ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { $intoType = $into !== null - ? $this->dataReturnTypeFactory->buildFromNamedType($into) - : $this->dataReturnTypeFactory->buildFromValue($items); + ? $this->dataReturnTypeFactory->buildFromNamedType($into, $dataClass, nullable: false) + : $this->dataReturnTypeFactory->buildFromValue($items, $dataClass, nullable: false); $collectable = $this->createFromCustomCreationMethod($dataClass, $creationContext, $items, $into); @@ -51,14 +52,18 @@ public function execute( $normalizedItems = $this->normalizeItems($items, $dataClass, $creationContext); + if(! $intoType->type instanceof NamedType) { + throw new Exception('Cannot collect into a union or intersection type'); + } + return match ($intoType->kind) { - DataTypeKind::Array => $this->normalizeToArray($normalizedItems), - DataTypeKind::Enumerable => new $intoType->type->name($this->normalizeToArray($normalizedItems)), + DataTypeKind::DataArray => $this->normalizeToArray($normalizedItems), + DataTypeKind::DataEnumerable => new $intoType->type->name($this->normalizeToArray($normalizedItems)), DataTypeKind::DataCollection => new $intoType->type->name($dataClass, $this->normalizeToArray($normalizedItems)), DataTypeKind::DataPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), DataTypeKind::DataCursorPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), - DataTypeKind::Paginator => $this->normalizeToPaginator($normalizedItems, $collectableMetaData), - DataTypeKind::CursorPaginator => $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData), + DataTypeKind::DataPaginator => $this->normalizeToPaginator($normalizedItems, $collectableMetaData), + DataTypeKind::DataCursorPaginator => $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData), default => throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->type->name) }; } diff --git a/src/Support/DataMethod.php b/src/Support/DataMethod.php index 19028a28..224cb060 100644 --- a/src/Support/DataMethod.php +++ b/src/Support/DataMethod.php @@ -16,7 +16,7 @@ public function __construct( public readonly bool $isStatic, public readonly bool $isPublic, public readonly CustomCreationMethodType $customCreationMethodType, - public readonly ?DataReturnType $returnType, + public readonly ?DataType $returnType, ) { } diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index fa6e440c..1bd86c85 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -14,7 +14,7 @@ class DataProperty public function __construct( public readonly string $name, public readonly string $className, - public readonly DataType $type, + public readonly DataPropertyType $type, public readonly bool $validate, public readonly bool $computed, public readonly bool $hidden, diff --git a/src/Support/DataPropertyType.php b/src/Support/DataPropertyType.php new file mode 100644 index 00000000..81746f23 --- /dev/null +++ b/src/Support/DataPropertyType.php @@ -0,0 +1,30 @@ +|null $lazyType + */ + public function __construct( + Type $type, + public readonly bool $isOptional, + bool $isNullable, + bool $isMixed, + public readonly ?string $lazyType, + // @note for now we have a one data type per type rule + // Meaning a type can be a data object of some type, data collection of some type or something else + // If we want to support multiple types in the future all we need to do is replace calls to these + // properties and handle everything correctly + DataTypeKind $kind, + public readonly ?string $dataClass, + public readonly ?string $dataCollectableClass, + ) { + parent::__construct($type, $isNullable, $isMixed, $kind); + } +} diff --git a/src/Support/DataReturnType.php b/src/Support/DataReturnType.php deleted file mode 100644 index 3ad50e1c..00000000 --- a/src/Support/DataReturnType.php +++ /dev/null @@ -1,20 +0,0 @@ -type->acceptsType($type); - } -} diff --git a/src/Support/DataType.php b/src/Support/DataType.php index b72c7ad1..dd480a72 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -3,29 +3,17 @@ namespace Spatie\LaravelData\Support; use Spatie\LaravelData\Enums\DataTypeKind; -use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Support\Types\Type; class DataType { - /** - * @param class-string|null $lazyType - */ public function __construct( public readonly Type $type, - public readonly bool $isOptional, public readonly bool $isNullable, public readonly bool $isMixed, - public readonly ?string $lazyType, - // @note for now we have a one data type per type rule - // Meaning a type can be a data object of some type, data collection of some type or something else - // If we want to support multiple types in the future all we need to do is replace calls to these - // properties and handle everything correctly public readonly DataTypeKind $kind, - public readonly ?string $dataClass, - public readonly ?string $dataCollectableClass, - ) { - + ) + { } public function findAcceptedTypeForBaseType(string $class): ?string diff --git a/src/Support/Factories/DataMethodFactory.php b/src/Support/Factories/DataMethodFactory.php index 1a9d20a4..6f34fa5d 100644 --- a/src/Support/Factories/DataMethodFactory.php +++ b/src/Support/Factories/DataMethodFactory.php @@ -8,7 +8,7 @@ use ReflectionParameter; use Spatie\LaravelData\Enums\CustomCreationMethodType; use Spatie\LaravelData\Support\DataMethod; -use Spatie\LaravelData\Support\DataReturnType; +use Spatie\LaravelData\Support\DataType; class DataMethodFactory { @@ -24,7 +24,7 @@ public function build( ReflectionClass $reflectionClass, ): DataMethod { $returnType = $reflectionMethod->getReturnType() - ? $this->returnTypeFactory->build($reflectionMethod->getReturnType()) + ? $this->returnTypeFactory->build($reflectionMethod, $reflectionClass) : null; return new DataMethod( @@ -71,7 +71,7 @@ public function buildConstructor( protected function resolveCustomCreationMethodType( ReflectionMethod $method, - ?DataReturnType $returnType, + ?DataType $returnType, ): CustomCreationMethodType { if (! $method->isStatic() || ! $method->isPublic() @@ -86,7 +86,7 @@ protected function resolveCustomCreationMethodType( return CustomCreationMethodType::Object; } - if (str_starts_with($method->name, 'collect') && $returnType?->kind->isDataCollectable()) { + if (str_starts_with($method->name, 'collect') && $returnType) { return CustomCreationMethodType::Collection; } diff --git a/src/Support/Factories/DataPropertyFactory.php b/src/Support/Factories/DataPropertyFactory.php index 9a12c622..d58ff2dd 100644 --- a/src/Support/Factories/DataPropertyFactory.php +++ b/src/Support/Factories/DataPropertyFactory.php @@ -65,7 +65,7 @@ public function build( return new DataProperty( name: $reflectionProperty->name, className: $reflectionProperty->class, - type: $this->typeFactory->build( + type: $this->typeFactory->buildProperty( $reflectionProperty->getType(), $reflectionClass, $reflectionProperty, diff --git a/src/Support/Factories/DataReturnTypeFactory.php b/src/Support/Factories/DataReturnTypeFactory.php index 91ca4929..2bbc62d2 100644 --- a/src/Support/Factories/DataReturnTypeFactory.php +++ b/src/Support/Factories/DataReturnTypeFactory.php @@ -2,52 +2,68 @@ namespace Spatie\LaravelData\Support\Factories; +use ReflectionClass; +use ReflectionMethod; use ReflectionNamedType; -use ReflectionType; -use Spatie\LaravelData\Support\DataReturnType; -use Spatie\LaravelData\Support\Types\NamedType; -use Spatie\LaravelData\Support\Types\Storage\AcceptedTypesStorage; -use TypeError; +use Spatie\LaravelData\Support\DataType; class DataReturnTypeFactory { - /** @var array */ + /** @var array */ public static array $store = []; - public function build(ReflectionType $type): DataReturnType + public function __construct( + protected DataTypeFactory $typeFactory + ) { + } + + public function build(ReflectionMethod $type, ReflectionClass|string $class): ?DataType { - if (! $type instanceof ReflectionNamedType) { - throw new TypeError('At the moment return types can only be of one type'); + if (! $type->hasReturnType()) { + return null; + } + + $returnType = $type->getReturnType(); + + if ($returnType instanceof ReflectionNamedType) { + return $this->buildFromNamedType($returnType->getName(), $class, $returnType->allowsNull()); } - return $this->buildFromNamedType($type->getName()); + return $this->typeFactory->build($returnType, $class, $returnType); } - public function buildFromNamedType(string $name): DataReturnType - { - if (array_key_exists($name, self::$store)) { - return self::$store[$name]; + public function buildFromNamedType( + string $name, + ReflectionClass|string $class, + bool $nullable, + ): DataType { + $storedName = $name.($nullable ? '?' : ''); + + if (array_key_exists($storedName, self::$store)) { + return self::$store[$storedName]; } $builtIn = in_array($name, ['array', 'bool', 'float', 'int', 'string', 'mixed', 'null']); - ['acceptedTypes' => $acceptedTypes, 'kind' => $kind] = AcceptedTypesStorage::getAcceptedTypesAndKind($name); - - return static::$store[$name] = new DataReturnType( - type: new NamedType( - name: $name, - builtIn: $builtIn, - acceptedTypes: $acceptedTypes, - kind: $kind, - dataClass: null, - dataCollectableClass: null, - ), - kind: $kind, + $dataType = $this->typeFactory->buildFromString( + $name, + $class, + $builtIn, + $nullable ); + + if ($name !== 'static' && $name !== 'self') { + self::$store[$storedName] = $dataType; + } + + return $dataType; } - public function buildFromValue(mixed $value): DataReturnType - { - return self::buildFromNamedType(get_debug_type($value)); + public function buildFromValue( + mixed $value, + ReflectionClass|string $class, + bool $nullable, + ): DataType { + return self::buildFromNamedType(get_debug_type($value), $class, $nullable); } } diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index c862a3cb..689a6b9d 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -19,6 +19,7 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotation; use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; +use Spatie\LaravelData\Support\DataPropertyType; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; @@ -30,7 +31,6 @@ use Spatie\LaravelData\Support\Types\Storage\AcceptedTypesStorage; use Spatie\LaravelData\Support\Types\Type; use Spatie\LaravelData\Support\Types\UnionType; -use TypeError; class DataTypeFactory { @@ -39,33 +39,23 @@ public function __construct( ) { } - public function build( + public function buildProperty( ?ReflectionType $reflectionType, - ReflectionClass|string $reflectionClass, + ReflectionClass|string $class, ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, ?Collection $attributes = null, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, - ): DataType { - $properties = match (true) { - $reflectionType === null => $this->inferPropertiesForNoneType(), - $reflectionType instanceof ReflectionNamedType => $this->inferPropertiesForSingleType( - $reflectionType, - $reflectionClass, - $typeable, - $attributes, - $classDefinedDataCollectableAnnotation - ), - $reflectionType instanceof ReflectionUnionType || $reflectionType instanceof ReflectionIntersectionType => $this->inferPropertiesForCombinationType( - $reflectionType, - $reflectionClass, - $typeable, - $attributes, - $classDefinedDataCollectableAnnotation - ), - default => throw new TypeError('Invalid reflection type') - }; + ): DataPropertyType { + $properties = $this->infer( + reflectionType: $reflectionType, + class: $class, + typeable: $typeable, + attributes: $attributes, + classDefinedDataCollectableAnnotation: $classDefinedDataCollectableAnnotation, + inferForProperty: true, + ); - return new DataType( + return new DataPropertyType( type: $properties['type'], isOptional: $properties['isOptional'], isNullable: $reflectionType?->allowsNull() ?? true, @@ -77,6 +67,100 @@ public function build( ); } + public function build( + ?ReflectionType $reflectionType, + ReflectionClass|string $class, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ): DataType { + $properties = $this->infer( + reflectionType: $reflectionType, + class: $class, + typeable: $typeable, + attributes: null, + classDefinedDataCollectableAnnotation: null, + inferForProperty: false, + ); + + return new DataType( + type: $properties['type'], + isNullable: $reflectionType?->allowsNull() ?? true, + isMixed: $properties['isMixed'], + kind: $properties['kind'], + ); + } + + public function buildFromString( + string $type, + ReflectionClass|string $class, + bool $isBuiltIn, + bool $isNullable = false, + ): DataType { + $properties = $this->inferPropertiesForNamedType( + name: $type, + builtIn: $isBuiltIn, + class: $class, + typeable: $type, + attributes: null, + classDefinedDataCollectableAnnotation: null, + inferForProperty: false, + ); + + return new DataType( + type: $properties['type'], + isNullable: $isNullable, + isMixed: $properties['isMixed'], + kind: $properties['kind'], + ); + } + + /** + * @return array{ + * type: Type, + * isMixed: bool, + * lazyType: ?string, + * isOptional: bool, + * kind: DataTypeKind, + * dataClass: ?string, + * dataCollectableClass: ?string + * } + */ + protected function infer( + ?ReflectionType $reflectionType, + ReflectionClass|string $class, + ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ?Collection $attributes, + ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + bool $inferForProperty, + ): array { + if ($reflectionType === null) { + return $this->inferPropertiesForNoneType(); + } + + if ($reflectionType instanceof ReflectionNamedType) { + return $this->inferPropertiesForSingleType( + $reflectionType, + $class, + $typeable, + $attributes, + $classDefinedDataCollectableAnnotation, + $inferForProperty, + ); + } + + if ($reflectionType instanceof ReflectionUnionType || $reflectionType instanceof ReflectionIntersectionType) { + return $this->inferPropertiesForCombinationType( + $reflectionType, + $class, + $typeable, + $attributes, + $classDefinedDataCollectableAnnotation, + $inferForProperty, + ); + } + + throw new Exception('Invalid reflected type'); + } + /** * @return array{ * type: NamedType, @@ -120,23 +204,24 @@ protected function inferPropertiesForNoneType(): array * dataClass: ?string, * dataCollectableClass: ?string * } - * */ protected function inferPropertiesForSingleType( ReflectionNamedType $reflectionType, - ReflectionClass|string $reflectionClass, + ReflectionClass|string $class, ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + bool $inferForProperty, ): array { return [ ...$this->inferPropertiesForNamedType( $reflectionType->getName(), $reflectionType->isBuiltin(), - $reflectionClass, + $class, $typeable, $attributes, - $classDefinedDataCollectableAnnotation + $classDefinedDataCollectableAnnotation, + $inferForProperty, ), 'isOptional' => false, 'lazyType' => null, @@ -155,20 +240,21 @@ protected function inferPropertiesForSingleType( protected function inferPropertiesForNamedType( string $name, bool $builtIn, - ReflectionClass|string $reflectionClass, + ReflectionClass|string $class, ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + bool $inferForProperty, ): array { if ($name === 'self' || $name === 'static') { - $name = is_string($reflectionClass) ? $reflectionClass : $reflectionClass->getName(); + $name = is_string($class) ? $class : $class->getName(); } $isMixed = $name === 'mixed'; ['acceptedTypes' => $acceptedTypes, 'kind' => $kind] = AcceptedTypesStorage::getAcceptedTypesAndKind($name); - if ($kind === DataTypeKind::Default) { + if ($kind === DataTypeKind::Default || ($inferForProperty === false && $kind->isDataCollectable())) { return [ 'type' => new NamedType( name: $name, @@ -232,7 +318,7 @@ protected function inferPropertiesForNamedType( ]; } - if (in_array($kind, [DataTypeKind::Array, DataTypeKind::Paginator, DataTypeKind::CursorPaginator, DataTypeKind::Enumerable])) { + if (in_array($kind, [DataTypeKind::DataArray, DataTypeKind::DataPaginator, DataTypeKind::DataCursorPaginator, DataTypeKind::DataEnumerable])) { return [ 'type' => new NamedType( name: $name, @@ -266,10 +352,11 @@ protected function inferPropertiesForNamedType( */ protected function inferPropertiesForCombinationType( ReflectionUnionType|ReflectionIntersectionType $reflectionType, - ReflectionClass|string $reflectionClass, + ReflectionClass|string $class, ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, ?Collection $attributes, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation, + bool $inferForProperty, ): array { $isMixed = false; $isOptional = false; @@ -285,10 +372,11 @@ protected function inferPropertiesForCombinationType( if ($reflectionSubType::class === ReflectionUnionType::class || $reflectionSubType::class === ReflectionIntersectionType::class) { $properties = $this->inferPropertiesForCombinationType( $reflectionSubType, - $reflectionClass, + $class, $typeable, $attributes, - $classDefinedDataCollectableAnnotation + $classDefinedDataCollectableAnnotation, + $inferForProperty ); $isMixed = $isMixed || $properties['isMixed']; @@ -305,7 +393,6 @@ protected function inferPropertiesForCombinationType( } /** @var ReflectionNamedType $reflectionSubType */ - $name = $reflectionSubType->getName(); if ($name === Optional::class) { @@ -318,7 +405,7 @@ protected function inferPropertiesForCombinationType( continue; } - if (in_array($name, [Lazy::class, DefaultLazy::class, ClosureLazy::class, ConditionalLazy::class, RelationalLazy::class, InertiaLazy::class])) { + if ($inferForProperty && in_array($name, [Lazy::class, DefaultLazy::class, ClosureLazy::class, ConditionalLazy::class, RelationalLazy::class, InertiaLazy::class])) { $lazyType = $name; continue; @@ -327,10 +414,11 @@ protected function inferPropertiesForCombinationType( $properties = $this->inferPropertiesForNamedType( $reflectionSubType->getName(), $reflectionSubType->isBuiltin(), - $reflectionClass, + $class, $typeable, $attributes, - $classDefinedDataCollectableAnnotation + $classDefinedDataCollectableAnnotation, + $inferForProperty ); $isMixed = $isMixed || $properties['isMixed']; diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 80154255..cd037dbc 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -108,9 +108,9 @@ protected function resolveTypeForProperty( } $collectionType = match ($dataProperty->type->kind) { - DataTypeKind::DataCollection, DataTypeKind::Array, DataTypeKind::Enumerable => $this->defaultCollectionType($dataProperty->type->dataClass), - DataTypeKind::Paginator, DataTypeKind::DataPaginatedCollection => $this->paginatedCollectionType($dataProperty->type->dataClass), - DataTypeKind::CursorPaginator, DataTypeKind::DataCursorPaginatedCollection => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), + DataTypeKind::DataCollection, DataTypeKind::DataArray, DataTypeKind::DataEnumerable => $this->defaultCollectionType($dataProperty->type->dataClass), + DataTypeKind::DataPaginator, DataTypeKind::DataPaginatedCollection => $this->paginatedCollectionType($dataProperty->type->dataClass), + DataTypeKind::DataCursorPaginator, DataTypeKind::DataCursorPaginatedCollection => $this->cursorPaginatedCollectionType($dataProperty->type->dataClass), default => throw new RuntimeException('Cannot end up here since the type is dataCollectable') }; diff --git a/src/Support/Types/Storage/AcceptedTypesStorage.php b/src/Support/Types/Storage/AcceptedTypesStorage.php index 4af27c92..31fb773f 100644 --- a/src/Support/Types/Storage/AcceptedTypesStorage.php +++ b/src/Support/Types/Storage/AcceptedTypesStorage.php @@ -55,13 +55,13 @@ protected static function resolveDataTypeKind(string $name, array $acceptedTypes { return match (true) { in_array(BaseData::class, $acceptedTypes) => DataTypeKind::DataObject, - $name === 'array' => DataTypeKind::Array, - in_array(Enumerable::class, $acceptedTypes) => DataTypeKind::Enumerable, + $name === 'array' => DataTypeKind::DataArray, + in_array(Enumerable::class, $acceptedTypes) => DataTypeKind::DataEnumerable, in_array(DataCollection::class, $acceptedTypes) || $name === DataCollection::class => DataTypeKind::DataCollection, in_array(PaginatedDataCollection::class, $acceptedTypes) || $name === PaginatedDataCollection::class => DataTypeKind::DataPaginatedCollection, in_array(CursorPaginatedDataCollection::class, $acceptedTypes) || $name === CursorPaginatedDataCollection::class => DataTypeKind::DataCursorPaginatedCollection, - in_array(Paginator::class, $acceptedTypes) || in_array(AbstractPaginator::class, $acceptedTypes) => DataTypeKind::Paginator, - in_array(CursorPaginator::class, $acceptedTypes) || in_array(AbstractCursorPaginator::class, $acceptedTypes) => DataTypeKind::CursorPaginator, + in_array(Paginator::class, $acceptedTypes) || in_array(AbstractPaginator::class, $acceptedTypes) => DataTypeKind::DataPaginator, + in_array(CursorPaginator::class, $acceptedTypes) || in_array(AbstractCursorPaginator::class, $acceptedTypes) => DataTypeKind::DataCursorPaginator, default => DataTypeKind::Default, }; } diff --git a/tests/Factories/FakeDataStructureFactory.php b/tests/Factories/FakeDataStructureFactory.php index 3739e946..efd2a7a5 100644 --- a/tests/Factories/FakeDataStructureFactory.php +++ b/tests/Factories/FakeDataStructureFactory.php @@ -79,12 +79,4 @@ public static function parameter( return $factory->build($parameter, $parameter->getDeclaringClass()); } - - public static function returnType( - ReflectionMethod $method, - ): ?DataType { - $factory = static::$returnTypeFactory ??= app(DataReturnTypeFactory::class); - - return $factory->build($method->getReturnType()); - } } diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index 7e5049f3..a5ab4e23 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -3,6 +3,7 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; +use Spatie\LaravelData\Support\DataPropertyType; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -28,6 +29,8 @@ public function __construct( ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() ->type->toBeInstanceOf(DataType::class) + ->type->type->name->toBe('string') + ->type->isNullable->toBeFalse() ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'withoutType'); @@ -39,6 +42,8 @@ public function __construct( ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() ->type->toBeInstanceOf(DataType::class) + ->type->isMixed->toBeTrue() + ->type->isNullable->toBeTrue() ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'property'); @@ -50,6 +55,8 @@ public function __construct( ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() ->type->toBeInstanceOf(DataType::class) + ->type->type->name->toBe('string') + ->type->isNullable->toBeFalse() ->type->type->isCreationContext()->toBeFalse(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'creationContext'); @@ -61,6 +68,8 @@ public function __construct( ->hasDefaultValue->toBeFalse() ->defaultValue->toBeNull() ->type->toBeInstanceOf(DataType::class) + ->type->type->name->toBe(CreationContext::class) + ->type->isNullable->toBeFalse() ->type->type->isCreationContext()->toBeTrue(); $reflection = new ReflectionParameter([$class::class, '__construct'], 'propertyWithDefault'); @@ -72,5 +81,7 @@ public function __construct( ->hasDefaultValue->toBeTrue() ->defaultValue->toEqual('hello') ->type->toBeInstanceOf(DataType::class) + ->type->type->name->toBe('string') + ->type->isNullable->toBeFalse() ->type->type->isCreationContext()->toBeFalse(); }); diff --git a/tests/Support/DataTypeTest.php b/tests/Support/DataPropertyTypeTest.php similarity index 95% rename from tests/Support/DataTypeTest.php rename to tests/Support/DataPropertyTypeTest.php index b2cdc420..2eab9eeb 100644 --- a/tests/Support/DataTypeTest.php +++ b/tests/Support/DataPropertyTypeTest.php @@ -27,7 +27,7 @@ use Spatie\LaravelData\Lazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\DataType; +use Spatie\LaravelData\Support\DataPropertyType; use Spatie\LaravelData\Support\Lazy\ClosureLazy; use Spatie\LaravelData\Support\Lazy\ConditionalLazy; use Spatie\LaravelData\Support\Lazy\InertiaLazy; @@ -41,7 +41,7 @@ use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithMappedProperty; -function resolveDataType(object $class, string $property = 'property'): DataType +function resolveDataType(object $class, string $property = 'property'): DataPropertyType { $class = FakeDataStructureFactory::class($class); @@ -484,7 +484,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBeNull() - ->kind->toBe(DataTypeKind::Array) + ->kind->toBe(DataTypeKind::DataArray) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe('array') ->getAcceptedTypes()->toHaveKeys(['array']); @@ -493,7 +493,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe('array') ->builtIn->toBeTrue() - ->kind->toBe(DataTypeKind::Array) + ->kind->toBe(DataTypeKind::DataArray) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe('array'); }); @@ -509,7 +509,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBe(Lazy::class) - ->kind->toBe(DataTypeKind::Array) + ->kind->toBe(DataTypeKind::DataArray) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe('array') ->getAcceptedTypes()->toHaveKeys(['array']); @@ -518,7 +518,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe('array') ->builtIn->toBeTrue() - ->kind->toBe(DataTypeKind::Array) + ->kind->toBe(DataTypeKind::DataArray) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe('array'); }); @@ -534,7 +534,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBeNull() - ->kind->toBe(DataTypeKind::Enumerable) + ->kind->toBe(DataTypeKind::DataEnumerable) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(Collection::class) ->getAcceptedTypes()->toHaveKeys([Collection::class]); @@ -543,7 +543,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe(Collection::class) ->builtIn->toBeFalse() - ->kind->toBe(DataTypeKind::Enumerable) + ->kind->toBe(DataTypeKind::DataEnumerable) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(Collection::class); }); @@ -559,7 +559,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBe(Lazy::class) - ->kind->toBe(DataTypeKind::Enumerable) + ->kind->toBe(DataTypeKind::DataEnumerable) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(Collection::class) ->getAcceptedTypes()->toHaveKeys([Collection::class]); @@ -568,7 +568,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe(Collection::class) ->builtIn->toBeFalse() - ->kind->toBe(DataTypeKind::Enumerable) + ->kind->toBe(DataTypeKind::DataEnumerable) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(Collection::class); }); @@ -584,7 +584,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBeNull() - ->kind->toBe(DataTypeKind::Paginator) + ->kind->toBe(DataTypeKind::DataPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(LengthAwarePaginator::class) ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); @@ -593,7 +593,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe(LengthAwarePaginator::class) ->builtIn->toBeFalse() - ->kind->toBe(DataTypeKind::Paginator) + ->kind->toBe(DataTypeKind::DataPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(LengthAwarePaginator::class); }); @@ -609,7 +609,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBe(Lazy::class) - ->kind->toBe(DataTypeKind::Paginator) + ->kind->toBe(DataTypeKind::DataPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(LengthAwarePaginator::class) ->getAcceptedTypes()->toHaveKeys([LengthAwarePaginator::class]); @@ -618,7 +618,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe(LengthAwarePaginator::class) ->builtIn->toBeFalse() - ->kind->toBe(DataTypeKind::Paginator) + ->kind->toBe(DataTypeKind::DataPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(LengthAwarePaginator::class); }); @@ -634,7 +634,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBeNull() - ->kind->toBe(DataTypeKind::CursorPaginator) + ->kind->toBe(DataTypeKind::DataCursorPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(CursorPaginator::class) ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); @@ -643,7 +643,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe(CursorPaginator::class) ->builtIn->toBeFalse() - ->kind->toBe(DataTypeKind::CursorPaginator) + ->kind->toBe(DataTypeKind::DataCursorPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(CursorPaginator::class); }); @@ -659,7 +659,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->isNullable->toBeFalse() ->isMixed->toBeFalse() ->lazyType->toBe(Lazy::class) - ->kind->toBe(DataTypeKind::CursorPaginator) + ->kind->toBe(DataTypeKind::DataCursorPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(CursorPaginator::class) ->getAcceptedTypes()->toHaveKeys([CursorPaginator::class]); @@ -668,7 +668,7 @@ function resolveDataType(object $class, string $property = 'property'): DataType ->toBeInstanceOf(NamedType::class) ->name->toBe(CursorPaginator::class) ->builtIn->toBeFalse() - ->kind->toBe(DataTypeKind::CursorPaginator) + ->kind->toBe(DataTypeKind::DataCursorPaginator) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe(CursorPaginator::class); }); @@ -1075,7 +1075,7 @@ public function __construct( $type = resolveDataType(new \TestDataTypeWithClassAnnotatedProperty([])); expect($type) - ->kind->toBe(DataTypeKind::Array) + ->kind->toBe(DataTypeKind::DataArray) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe('array'); }); @@ -1095,7 +1095,7 @@ public function __construct( $type = resolveDataType(new \TestDataTypeWithClassAnnotatedConstructorParam([])); expect($type) - ->kind->toBe(DataTypeKind::Array) + ->kind->toBe(DataTypeKind::DataArray) ->dataClass->toBe(SimpleData::class) ->dataCollectableClass->toBe('array'); }); @@ -1131,3 +1131,29 @@ public function __construct( expect($type)->lazyType->toBe(RelationalLazy::class); }); + +it('will mark an array, collection and paginators as a default type kind when no data collection was specified', function (){ + $type = resolveDataType(new class () { + public array $property; + }); + + expect($type)->kind->toBe(DataTypeKind::Default); + + $type = resolveDataType(new class () { + public Collection $property; + }); + + expect($type)->kind->toBe(DataTypeKind::Default); + + $type = resolveDataType(new class () { + public LengthAwarePaginator $property; + }); + + expect($type)->kind->toBe(DataTypeKind::Default); + + $type = resolveDataType(new class () { + public CursorPaginator $property; + }); + + expect($type)->kind->toBe(DataTypeKind::Default); +}); diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php index 75454fb7..482b439a 100644 --- a/tests/Support/DataReturnTypeTest.php +++ b/tests/Support/DataReturnTypeTest.php @@ -17,6 +17,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataReturnType; +use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; use Spatie\LaravelData\Support\Types\NamedType; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -37,31 +38,38 @@ public function dataCollection(): DataCollection { } + + public function nullableArray(): ?array + { + + } } it('can determine the return type from reflection', function ( string $methodName, string $typeName, mixed $value, - DataReturnType $expected + DataType $expected ) { $factory = app(DataReturnTypeFactory::class); - $reflection = (new ReflectionMethod(\TestReturnTypeSubject::class, $methodName))->getReturnType(); + $reflection = (new ReflectionMethod(\TestReturnTypeSubject::class, $methodName)); - expect($factory->build($reflection))->toEqual($expected); + expect($factory->build($reflection, TestReturnTypeSubject::class))->toEqual($expected); - expect($factory->buildFromNamedType($typeName))->toEqual($expected); + expect($factory->buildFromNamedType($typeName, TestReturnTypeSubject::class, false))->toEqual($expected); - expect($factory->buildFromValue($value))->toEqual($expected); + expect($factory->buildFromValue($value, TestReturnTypeSubject::class, false))->toEqual($expected); })->with(function () { yield 'array' => [ 'methodName' => 'array', 'typeName' => 'array', 'value' => [], - new DataReturnType( - type: new NamedType('array', true, [], DataTypeKind::Array, null, null), - kind: DataTypeKind::Array, + new DataType( + type: new NamedType('array', true, [], DataTypeKind::DataArray, null, null), + isNullable: false, + isMixed: false, + kind: DataTypeKind::DataArray, ), ]; @@ -69,7 +77,7 @@ public function dataCollection(): DataCollection 'methodName' => 'collection', 'typeName' => Collection::class, 'value' => collect(), - new DataReturnType( + new DataType( type: new NamedType(Collection::class, false, [ ArrayAccess::class, CanBeEscapedWhenCastToString::class, @@ -81,8 +89,10 @@ public function dataCollection(): DataCollection IteratorAggregate::class, Countable::class, Arrayable::class, - ], DataTypeKind::Enumerable, null, null), - kind: DataTypeKind::Enumerable, + ], DataTypeKind::DataEnumerable, null, null), + isNullable: false, + isMixed: false, + kind: DataTypeKind::DataEnumerable, ), ]; @@ -90,7 +100,7 @@ public function dataCollection(): DataCollection 'methodName' => 'dataCollection', 'typeName' => DataCollection::class, 'value' => new DataCollection(SimpleData::class, []), - new DataReturnType( + new DataType( type: new NamedType(DataCollection::class, false, [ DataCollectable::class, ArrayAccess::class, @@ -110,7 +120,37 @@ public function dataCollection(): DataCollection Responsable::class, ], DataTypeKind::DataCollection, null, null), + isNullable: false, + isMixed: false, kind: DataTypeKind::DataCollection, ), ]; }); + +it('will store return types in the factory as a caching mechanism', function (){ + $factory = app(DataReturnTypeFactory::class); + + $reflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'array'); + + $firstBuild = $factory->build($reflection, TestReturnTypeSubject::class); + $secondBuild = $factory->build($reflection, TestReturnTypeSubject::class); + + expect($firstBuild)->toBe($secondBuild); + expect(spl_object_id($firstBuild))->toBe(spl_object_id($secondBuild)); +}); + +it('will cache nullable and non nullable return types separately', function (){ + $factory = app(DataReturnTypeFactory::class); + + $firstReflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'array'); + $secondReflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'array'); + + $firstBuild = $factory->build($firstReflection, TestReturnTypeSubject::class); + $secondBuild = $factory->buildFromNamedType($secondReflection, TestReturnTypeSubject::class, true); + + expect($firstBuild)->isNullable->toBeFalse(); + expect($secondBuild)->isNullable->toBeTrue(); + + expect($firstBuild)->not->toBe($secondBuild); + expect(spl_object_id($firstBuild))->not->toBe(spl_object_id($secondBuild)); +}); From cad4c6ad88c3e33c0ca19faa9c26184162db8b65 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Mon, 22 Jan 2024 13:14:00 +0000 Subject: [PATCH 100/124] Fix styling --- src/DataCollection.php | 1 - src/Support/DataType.php | 3 +-- tests/Support/DataParameterTest.php | 1 - tests/Support/DataPropertyTypeTest.php | 2 +- tests/Support/DataReturnTypeTest.php | 5 ++--- 5 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/DataCollection.php b/src/DataCollection.php index 5e4fe370..d4bf71d0 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -5,7 +5,6 @@ use ArrayAccess; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; -use Illuminate\Support\LazyCollection; use Spatie\LaravelData\Concerns\BaseDataCollectable; use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\EnumerableMethods; diff --git a/src/Support/DataType.php b/src/Support/DataType.php index dd480a72..ea485f21 100644 --- a/src/Support/DataType.php +++ b/src/Support/DataType.php @@ -12,8 +12,7 @@ public function __construct( public readonly bool $isNullable, public readonly bool $isMixed, public readonly DataTypeKind $kind, - ) - { + ) { } public function findAcceptedTypeForBaseType(string $class): ?string diff --git a/tests/Support/DataParameterTest.php b/tests/Support/DataParameterTest.php index a5ab4e23..dfd67d37 100644 --- a/tests/Support/DataParameterTest.php +++ b/tests/Support/DataParameterTest.php @@ -3,7 +3,6 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; -use Spatie\LaravelData\Support\DataPropertyType; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\SimpleData; diff --git a/tests/Support/DataPropertyTypeTest.php b/tests/Support/DataPropertyTypeTest.php index 2eab9eeb..5f3c56e6 100644 --- a/tests/Support/DataPropertyTypeTest.php +++ b/tests/Support/DataPropertyTypeTest.php @@ -1132,7 +1132,7 @@ public function __construct( expect($type)->lazyType->toBe(RelationalLazy::class); }); -it('will mark an array, collection and paginators as a default type kind when no data collection was specified', function (){ +it('will mark an array, collection and paginators as a default type kind when no data collection was specified', function () { $type = resolveDataType(new class () { public array $property; }); diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php index 482b439a..9ca83a16 100644 --- a/tests/Support/DataReturnTypeTest.php +++ b/tests/Support/DataReturnTypeTest.php @@ -16,7 +16,6 @@ use Spatie\LaravelData\Contracts\WrappableData; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataTypeKind; -use Spatie\LaravelData\Support\DataReturnType; use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; use Spatie\LaravelData\Support\Types\NamedType; @@ -127,7 +126,7 @@ public function nullableArray(): ?array ]; }); -it('will store return types in the factory as a caching mechanism', function (){ +it('will store return types in the factory as a caching mechanism', function () { $factory = app(DataReturnTypeFactory::class); $reflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'array'); @@ -139,7 +138,7 @@ public function nullableArray(): ?array expect(spl_object_id($firstBuild))->toBe(spl_object_id($secondBuild)); }); -it('will cache nullable and non nullable return types separately', function (){ +it('will cache nullable and non nullable return types separately', function () { $factory = app(DataReturnTypeFactory::class); $firstReflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'array'); From 1b7ba58534f3c45bb3a1127e9727d8a34db11aa5 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 23 Jan 2024 10:23:35 +0100 Subject: [PATCH 101/124] Docs refactor --- config/data.php | 2 +- docs/_index.md | 2 +- docs/advanced-usage/creating-a-cast.md | 2 +- .../creating-a-rule-inferrer.md | 2 +- docs/advanced-usage/creating-a-transformer.md | 2 +- docs/advanced-usage/pipeline.md | 2 +- docs/advanced-usage/typescript.md | 2 +- docs/advanced-usage/use-with-inertia.md | 2 +- docs/as-a-data-transfer-object/casts.md | 2 +- docs/as-a-data-transfer-object/collections.md | 25 + docs/as-a-data-transfer-object/computed.md | 4 +- .../creating-a-data-object.md | 6 +- docs/as-a-data-transfer-object/defaults.md | 2 +- docs/as-a-data-transfer-object/factories.md | 73 ++ .../mapping-property-names.md | 4 +- docs/as-a-data-transfer-object/nesting.md | 30 +- .../optional-properties.md | 3 +- .../request-to-data-object.md | 641 +----------------- docs/as-a-resource/appending-properties.md | 89 +++ docs/as-a-resource/from-data-to-array.md | 132 ++++ docs/as-a-resource/from-data-to-resource.md | 335 ++------- docs/as-a-resource/lazy-properties.md | 2 +- docs/as-a-resource/mapping-property-names.md | 73 ++ docs/as-a-resource/transformers.md | 4 +- docs/as-a-resource/wrapping.md | 10 +- docs/getting-started/quickstart.md | 80 ++- docs/installation-setup.md | 43 +- docs/third-party-packages.md | 1 + docs/validation/auto-rule-inferring.md | 2 +- docs/validation/introduction.md | 32 +- docs/validation/nesting-data.md | 125 +++- docs/validation/skipping-validation.md | 4 + .../validation/using-validation-attributes.md | 3 +- src/DataPipes/ValidatePropertiesDataPipe.php | 6 +- src/Support/Creation/CreationContext.php | 2 +- .../Creation/CreationContextFactory.php | 20 +- ...idationType.php => ValidationStrategy.php} | 2 +- src/Transformers/Transformer.php | 6 +- tests/CreationFactoryTest.php | 2 +- tests/RequestTest.php | 7 - tests/ValidationTest.php | 6 +- tests/WrapTest.php | 10 + 42 files changed, 742 insertions(+), 1060 deletions(-) create mode 100644 docs/as-a-data-transfer-object/factories.md create mode 100644 docs/as-a-resource/appending-properties.md create mode 100644 docs/as-a-resource/from-data-to-array.md create mode 100644 docs/as-a-resource/mapping-property-names.md rename src/Support/Creation/{ValidationType.php => ValidationStrategy.php} (83%) diff --git a/config/data.php b/config/data.php index d0dca3e3..90fdbaf6 100644 --- a/config/data.php +++ b/config/data.php @@ -93,7 +93,7 @@ * method. By default, only when a request is passed the data is being validated. This * behaviour can be changed to always validate or to completely disable validation. */ - 'validation_type' => \Spatie\LaravelData\Support\Creation\ValidationType::OnlyRequests->value, + 'validation_strategy' => \Spatie\LaravelData\Support\Creation\ValidationStrategy::OnlyRequests->value, /** * When using an invalid include, exclude, only or except partial, the package will diff --git a/docs/_index.md b/docs/_index.md index ae97fe32..3bc44457 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -1,5 +1,5 @@ --- -title: v3 +title: v4 slogan: Powerful data objects for Laravel githubUrl: https://github.com/spatie/laravel-data branch: main diff --git a/docs/advanced-usage/creating-a-cast.md b/docs/advanced-usage/creating-a-cast.md index f2cfd090..fe0cc75a 100644 --- a/docs/advanced-usage/creating-a-cast.md +++ b/docs/advanced-usage/creating-a-cast.md @@ -14,7 +14,7 @@ interface Cast } ``` -The value that should be cast is given, and a `DataProperty` object which represents the property for which the value is cast. You can read more about the internal structures of the package [here](/docs/laravel-data/v3/advanced-usage/internal-structures). +The value that should be cast is given, and a `DataProperty` object which represents the property for which the value is cast. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures). Within the `context` array the complete payload is given. diff --git a/docs/advanced-usage/creating-a-rule-inferrer.md b/docs/advanced-usage/creating-a-rule-inferrer.md index 02495cbf..a37bb106 100644 --- a/docs/advanced-usage/creating-a-rule-inferrer.md +++ b/docs/advanced-usage/creating-a-rule-inferrer.md @@ -14,7 +14,7 @@ interface RuleInferrer } ``` -A collection of previous inferred rules is given, and a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v3/advanced-usage/internal-structures). +A collection of previous inferred rules is given, and a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures). The `RulesCollection` contains all the rules for the property represented as `ValidationRule` objects. diff --git a/docs/advanced-usage/creating-a-transformer.md b/docs/advanced-usage/creating-a-transformer.md index 071e91c8..95660b77 100644 --- a/docs/advanced-usage/creating-a-transformer.md +++ b/docs/advanced-usage/creating-a-transformer.md @@ -14,6 +14,6 @@ interface Transformer } ``` -The value that should be transformed is given, and a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v3/advanced-usage/internal-structures). +The value that should be transformed is given, and a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures). In the end, the transformer should return a transformed value. Please note that the given value of a transformer can never be `null`. diff --git a/docs/advanced-usage/pipeline.md b/docs/advanced-usage/pipeline.md index da7b62e6..333e11b1 100644 --- a/docs/advanced-usage/pipeline.md +++ b/docs/advanced-usage/pipeline.md @@ -55,7 +55,7 @@ The `handle` method has several arguments: - **payload** the non normalized payload - **class** the `DataClass` object for the data - object [more info](/docs/laravel-data/v3/advanced-usage/internal-structures) + object [more info](/docs/laravel-data/v4/advanced-usage/internal-structures) - **properties** the key-value properties which will be used to construct the data object When using a magic creation methods, the pipeline is not being used (since you manually overwrite how a data object is diff --git a/docs/advanced-usage/typescript.md b/docs/advanced-usage/typescript.md index 5cb3d686..66e5d29a 100644 --- a/docs/advanced-usage/typescript.md +++ b/docs/advanced-usage/typescript.md @@ -71,7 +71,7 @@ If you're using the `DtoTransformer` provided by the package, then be sure to pu Annotate each data object that you want to transform to Typescript with a `/** @typescript */` annotation or a `#[TypeScript]` attribute. -To [generate the typescript file](https://spatie.be/docs/typescript-transformer/v3/laravel/executing-the-transform-command) +To [generate the typescript file](https://spatie.be/docs/typescript-transformer/v4/laravel/executing-the-transform-command) , run this command: ```php diff --git a/docs/advanced-usage/use-with-inertia.md b/docs/advanced-usage/use-with-inertia.md index ae4a5e9f..ada92a26 100644 --- a/docs/advanced-usage/use-with-inertia.md +++ b/docs/advanced-usage/use-with-inertia.md @@ -15,7 +15,7 @@ return Inertia::render('Song', SongsData::from($song)); ## Lazy properties -This package supports [lazy](https://spatie.be/docs/laravel-data/v3/as-a-resource/lazy-properties) properties, which can be manually included or excluded. +This package supports [lazy](https://spatie.be/docs/laravel-data/v4/as-a-resource/lazy-properties) properties, which can be manually included or excluded. Inertia has a similar concept called [lazy data evaluation](https://inertiajs.com/partial-reloads#lazy-data-evaluation), where some properties wrapped in a closure only get evaluated and included in the response when explicitly asked. diff --git a/docs/as-a-data-transfer-object/casts.md b/docs/as-a-data-transfer-object/casts.md index 77b6dd6b..1a667201 100644 --- a/docs/as-a-data-transfer-object/casts.md +++ b/docs/as-a-data-transfer-object/casts.md @@ -118,4 +118,4 @@ Tip: we can also remove the `EnumCast` since the package will automatically cast ## Creating your own casts -It is possible to create your casts. You can read more about this in the [advanced chapter](/docs/laravel-data/v3/advanced-usage/creating-a-cast). +It is possible to create your casts. You can read more about this in the [advanced chapter](/docs/laravel-data/v4/advanced-usage/creating-a-cast). diff --git a/docs/as-a-data-transfer-object/collections.md b/docs/as-a-data-transfer-object/collections.md index 242ed4e6..df1a492e 100644 --- a/docs/as-a-data-transfer-object/collections.md +++ b/docs/as-a-data-transfer-object/collections.md @@ -101,6 +101,31 @@ There are a few requirements for this to work: - The method cannot be called **collect** - A **return type** must be defined +## Creating a data object with collection + +You can create a data object with a collection of data object just like you would create a data object with a nested data object: + +```php +use App\Data\SongData; +use Illuminate\Support\Collection; + +class AlbumData extends Data +{ + /** @var Collection */ + public Collection $songs; +} + +AlbumData::from([ + 'title' => 'Never Gonna Give You Up', + 'songs' => [ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], + ] +]); +``` + +Since the collection type here is a `Collection`, the package will automatically convert the array into a collection of data objects. + ## DataCollection's, PaginatedDataCollection's and CursorPaginatedCollection's The package also provides a few collection classes which can be used to create collections of data objects, it was a requirement to use these classes in the past versions of the package when nesting data objects collections in data objects. This is no longer the case and there are still valid use cases for them. diff --git a/docs/as-a-data-transfer-object/computed.md b/docs/as-a-data-transfer-object/computed.md index 2c10f95f..36ce8880 100644 --- a/docs/as-a-data-transfer-object/computed.md +++ b/docs/as-a-data-transfer-object/computed.md @@ -3,7 +3,7 @@ title: Computed values weight: 8 --- -Earlier we saw how default values can be set for a data object, the same approach can be used to set computed values, although slightly different: +Earlier we saw how default values can be set for a data object, sometimes you want to set a default value based on other properties. For example, you might want to set a `full_name` property based on a `first_name` and `last_name` property. You can do this by using a computed property: ```php use Spatie\LaravelData\Attributes\Computed; @@ -28,6 +28,8 @@ You can now do the following: SongData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche']); ``` +Please notice: the computed property won't be reevaluated when its dependencies change. If you want to update a computed property, you'll have to create a new object. + Again there are a few conditions for this approach: - You must always use a sole property, a property within the constructor definition won't work diff --git a/docs/as-a-data-transfer-object/creating-a-data-object.md b/docs/as-a-data-transfer-object/creating-a-data-object.md index 6bda5425..905eb84d 100644 --- a/docs/as-a-data-transfer-object/creating-a-data-object.md +++ b/docs/as-a-data-transfer-object/creating-a-data-object.md @@ -160,7 +160,7 @@ will try to create itself from the following types: - An *Arrayable* by calling `toArray` on it - An *array* -This list can be extended using extra normalizers, find more about it [here](https://spatie.be/docs/laravel-data/v3/advanced-usage/normalizers). +This list can be extended using extra normalizers, find more about it [here](https://spatie.be/docs/laravel-data/v4/advanced-usage/normalizers). When a data object cannot be created using magical methods or the default methods, a `CannotCreateData` exception will be thrown. @@ -185,6 +185,6 @@ You can ignore the magical creation methods when creating a data object as such: SongData::withoutMagicalCreationFrom($song); ``` -## Advanced creation +## Advanced creation using factories -Internally this package is using a pipeline to create a data object from something. This pipeline exists of steps which transform properties into a correct structure and it can be completely customized. You can read more about it [here](/docs/laravel-data/v3/advanced-usage/pipeline). +It is possible to configure how a data object is created, whether it will be validated, which casts to use and more. You can read more about it [here](/docs/laravel-data/v4/advanced-usage/factories). diff --git a/docs/as-a-data-transfer-object/defaults.md b/docs/as-a-data-transfer-object/defaults.md index a083d4bc..7bc5fe51 100644 --- a/docs/as-a-data-transfer-object/defaults.md +++ b/docs/as-a-data-transfer-object/defaults.md @@ -51,4 +51,4 @@ There are a few conditions for this approach: - You must always use a sole property, a property within the constructor definition won't work - The optional type is technically not required, but it's a good idea to use it otherwise the validation won't work -- Validation won't be performed on the default value, so make sure it's valid +- Validation won't be performed on the default value, so make sure it is valid diff --git a/docs/as-a-data-transfer-object/factories.md b/docs/as-a-data-transfer-object/factories.md new file mode 100644 index 00000000..dde8887d --- /dev/null +++ b/docs/as-a-data-transfer-object/factories.md @@ -0,0 +1,73 @@ +--- +title: Factories +weight: 10 +--- + +It is possible to automatically create data objects in all sorts of forms with this package. Sometimes a little bit more +control is required when a data object is being created. This is where factories come in. + +Factories allow you to create data objects like before but allow you to customize the creation process. + +For example, we can create a data object using a factory like this: + +```php +SongData::factory()->from(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); +``` + +Collecting a bunch of data objects using a factory can be done as such: + +```php +SongData::factory()->collect(Song::all()) +``` + +## Disable property name mapping + +We saw [earlier](/docs/laravel-data/v4/as-a-data-transfer-object/mapping-property-names) that it is possible to map +property names when creating a data object from an array. This can be disabled when using a factory: + +```php +ContractData::factory()->withoutPropertyNameMapping()->from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); // record_company will not be mapped to recordCompany +``` + +## Changing the validation strategy + +By default, the package will only validate Requests when creating a data object it is possible to change the validation +strategy to always validate for each type: + +```php +SongData::factory()->alwaysValidate()->from(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); +``` + +Or completely disable validation: + +```php +SongData::factory()->withoutValidation()->from(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); +``` + +## Disabling magic methods + +A data object can be created +using [magic methods](/docs/laravel-data/v4/as-a-data-transfer-object/creating-a-data-object.md#magical-creation) , this can be disabled +when using a factory: + +```php +SongData::factory()->withoutMagicalCreation()->from('Never gonna give you up'); // Won't work since the magical method creation is disabled +``` + +It is also possible to ignore the magical creation methods when creating a data object as such: + +```php +SongData::factory()->ignoreMagicalMethod('fromString')->from('Never gonna give you up'); // Won't work since the magical method is ignored +``` + +## Adding additional global casts + +When creating a data object, it is possible to add additional casts to the data object: + +```php +SongData::factory()->withCast('string', StringToUpperCast::class)->from(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); +``` + +These casts will not replace the other global casts defined in the `data.php` config file, they will though run before +the other global casts. You define them just like you would define them in the config file, the first parameter is the +type of the property that should be cast and the second parameter is the cast class. diff --git a/docs/as-a-data-transfer-object/mapping-property-names.md b/docs/as-a-data-transfer-object/mapping-property-names.md index 6ba5be7b..c089bc74 100644 --- a/docs/as-a-data-transfer-object/mapping-property-names.md +++ b/docs/as-a-data-transfer-object/mapping-property-names.md @@ -20,7 +20,7 @@ class ContractData extends Data Creating the data object can now be done as such: ```php -SongData::from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); +ContractData::from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); ``` Changing all property names in a data object to snake_case in the data the object is created from can be done as such: @@ -37,7 +37,7 @@ class ContractData extends Data } ``` -You can also use the `MapName` attribute when you want to combine input (see [transforming data objects](https://spatie.be/docs/laravel-data/v3/as-a-resource/from-data-to-resource#mapping-property-names)) and output property name mapping: +You can also use the `MapName` attribute when you want to combine input (see [transforming data objects](/docs/laravel-data/v4/as-a-resource/from-data-to-resource#mapping-property-names)) and output property name mapping: ```php #[MapName(SnakeCaseMapper::class)] diff --git a/docs/as-a-data-transfer-object/nesting.md b/docs/as-a-data-transfer-object/nesting.md index 434d9b4e..b71613d8 100644 --- a/docs/as-a-data-transfer-object/nesting.md +++ b/docs/as-a-data-transfer-object/nesting.md @@ -115,11 +115,11 @@ The same is true for Laravel collections, but be sure to use two generic paramet ```php use App\Data\SongData; -use \Illuminate\Support\Collection; +use Illuminate\Support\Collection; class AlbumData extends Data { - /** @var Collection */ + /** @var Collection */ public Collection $songs; } ``` @@ -139,29 +139,3 @@ class AlbumData extends Data ``` This was the old way to define the type of data objects that will be stored within a collection. It is still supported, but we recommend using the annotation. - -### Creating a data object with collection - -You can create a data object with a collection of data object just like you would create a data object with a nested data object: - -```php -new AlbumData( - 'Never gonna give you up', - [ - new SongData('Never gonna give you up', 'Rick Astley'), - new SongData('Giving up on love', 'Rick Astley'), - ] -); -``` - -Or use the magical creation which will automatically create the data objects for you and also works with collections: - -```php -AlbumData::from([ - 'title' => 'Never Gonna Give You Up', - 'songs' => [ - ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], - ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], - ] -]); -``` diff --git a/docs/as-a-data-transfer-object/optional-properties.md b/docs/as-a-data-transfer-object/optional-properties.md index b37beeea..94f10af8 100644 --- a/docs/as-a-data-transfer-object/optional-properties.md +++ b/docs/as-a-data-transfer-object/optional-properties.md @@ -45,7 +45,8 @@ class SongData extends Data ) { } - public static function fromTitle(string $title): static{ + public static function fromTitle(string $title): static + { return new self($title, Optional::create()); } } diff --git a/docs/as-a-data-transfer-object/request-to-data-object.md b/docs/as-a-data-transfer-object/request-to-data-object.md index 621b3008..971fafe5 100644 --- a/docs/as-a-data-transfer-object/request-to-data-object.md +++ b/docs/as-a-data-transfer-object/request-to-data-object.md @@ -63,335 +63,9 @@ class UpdateSongController } ``` -We have a complete section within these docs dedicated to validation, you can find it [here](/docs/laravel-data/v3/validation). +We have a complete section within these docs dedicated to validation, you can find it [here](/docs/laravel-data/v4/validation). -## Using validation - -When creating a data object from a request, the package can also validate the values from the request that will be used -to construct the data object. - -The package automatically infers rules for certain properties. For example, a `?string` property will automatically have the `nullable` and `string` rules. - -Be aware, first the rules will be generated from the data object you're trying to create, then if the validation is successful a data object will be created with the validated data. This means validation will be run before a data object exists. - -It is possible to add extra rules as attributes to properties of a data object: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - #[Max(20)] - public string $artist, - ) { - } -} -``` - -When you provide an artist with a length of more than 20 characters, the validation will fail just like it would when -you created a custom request class for the endpoint. - -You can find a complete list of available rules [here](/docs/laravel-data/v3/advanced-usage/validation-attributes). - -If you want to have full control over the rules, you can also define them in a dedicated `rules` method on the data object (see later). - -### Referencing route parameters - -Sometimes you need a value within your validation attribute which is a route parameter. -Like the example below where the id should be unique ignoring the current id: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - #[Unique('songs', ignore: new RouteParameterReference('song'))] - public int $id, - ) { - } -} -``` - -If the parameter is a model and another property should be used, then you can do the following: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - #[Unique('songs', ignore: new RouteParameterReference('song', 'uuid'))] - public string $uuid, - ) { - } -} -``` - -### Referencing other fields - -It is possible to reference other fields in validation attributes: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - #[RequiredUnless('title', 'Never Gonna Give You Up')] - public string $artist, - ) { - } -} -``` - -These references are always relative to the current data object. So when being nested like this: - -```php -class AlbumData extends Data -{ - public function __construct( - public string $album_name, - public SongData $song, - ) { - } -} -``` - -The generated rules will look like this: - -```php -[ - 'album_name' => ['required', 'string'], - 'songs' => ['required', 'array'], - 'song.title' => ['required', 'string'], - 'song.artist' => ['string', 'required_if:song.title,"Never Gonna Give You Up"'], -] -``` - -If you want to reference fields starting from the root data object you can do the following: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - #[RequiredUnless(new FieldReference('album', fromRoot: true), 'Whenever You Need Somebody')] - public string $artist, - ) { - } -} -``` - -The rules will now look like this: - -```php -[ - 'album_name' => ['required', 'string'], - 'songs' => ['required', 'array'], - 'song.title' => ['required', 'string'], - 'song.artist' => ['string', 'required_if:album_name,"Whenever You Need Somebody"'], -] -``` - -### Rule attribute - -One special attribute is the `Rule` attribute. With it, you can write rules just like you would when creating a custom -Laravel request: - -```php -// using an array -#[Rule(['required', 'string'])] -public string $property - -// using a string -#[Rule('required|string')] -public string $property - -// using multiple arguments -#[Rule('required', 'string')] -public string $property -``` - -It is also possible to write rules down in a dedicated method on the data object. This can come in handy when you want -to construct a custom rule object which isn't possible with attributes: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function rules(): array - { - return [ - 'title' => ['required', 'string'], - 'artist' => ['required', 'string'], - ]; - } -} -``` - -By overwriting a property's rules within the `rules` method, no other rules will be inferred automatically anymore for that property. - -> Always use the array syntax for defining rules and not a single string which spits the rules by | characters. -> This is needed when using regexes those | can be seen as part of the regex - -It is even possible to use the validationAttribute objects within the `rules` method: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function rules(): array - { - return [ - 'title' => [new Required(), new StringType()], - 'artist' => [new Required(), new StringType()], - ]; - } -} -``` - -Rules defined within the `rules` method will always overwrite automatically generated rules. - -You can even add dependencies to be automatically injected: - -```php -use SongSettingsRepository; - -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function rules(SongSettingsRepository $settings): array - { - return [ - 'title' => [new RequiredIf($settings->forUser(auth()->user())->title_required), new StringType()], - 'artist' => [new Required(), new StringType()], - ]; - } -} -``` - -Sometimes a bit more context is required, in such case a `ValidationContext` parameter can be injected as such: -Additionally, if you need to access the data payload, you can use `$payload` parameter: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function rules(ValidationContext $context): array - { - return [ - 'title' => ['required'], - 'artist' => Rule::requiredIf($context->fullPayload['title'] !== 'Never Gonna Give You Up'), - ]; - } -} -``` - -By default, the provided payload is the whole request payload provided to the data object. -If you want to generate rules in nested data objects then a relative payload can be more useful: - -```php -class AlbumData extends Data -{ - public function __construct( - public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, - ) { - } -} - -class SongData extends Data -{ - public function __construct( - public string $title, - public ?string $artist, - ) { - } - - public static function rules(ValidationContext $context): array - { - return [ - 'title' => ['required'], - 'artist' => Rule::requiredIf($context->payload['title'] !== 'Never Gonna Give You Up'), - ]; - } -} -``` - -When providing such payload: - -```php -[ - 'title' => 'Best songs ever made', - 'songs' => [ - ['title' => 'Never Gonna Give You Up'], - ['title' => 'Heroes', 'artist' => 'David Bowie'], - ], -]; -``` - -The rules will be: - -```php -[ - 'title' => ['string', 'required'], - 'songs' => ['present', 'array'], - 'songs.*.title' => ['string', 'required'], - 'songs.*.artist' => ['string', 'nullable'], - 'songs.*' => [NestedRules(...)], -] -``` - -It is also possible to retrieve the current path in the data object chain we're generating rules for right now by calling `$context->path`. In the case of our previous example this would be `songs.0` and `songs.1`; - -Make sure the name of the parameter is `$context` in the `rules` method, otherwise no context will be injected. - -## Mapping a request onto a data object - -By default, the package will do a one to one mapping from request to the data object, which means that for each property -within the data object, a value with the same key will be searched within the request values. - -If you want to customize this mapping, then you can always add a magical creation method like this: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function fromRequest(Request $request): static - { - return new self( - $request->input('title_of_song'), - $request->input('artist_name') - ); - } -} -``` - -### Getting the data object filled with request data from anywhere +## Getting the data object filled with request data from anywhere You can resolve a data object from the container. @@ -402,278 +76,6 @@ app(SongData::class); We resolve a data object from the container, it's properties will allready be filled by the values of the request with matching key names. If the request contains data that is not compatible with the data object, a validation exception will be thrown. -### Automatically inferring rules for properties - -Since we have such strongly typed data objects, we can infer some validation rules from them. Rule inferrers will take -information about the type of the property and will create validation rules from that information. - -Rule inferrers are configured in the `data.php` config file: - -```php -/* - * Rule inferrers can be configured here. They will automatically add - * validation rules to properties of a data object based upon - * the type of the property. - */ -'rule_inferrers' => [ - Spatie\LaravelData\RuleInferrers\SometimesRuleInferrer::class, - Spatie\LaravelData\RuleInferrers\NullableRuleInferrer::class, - Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer::class, - Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer::class, - Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer::class, -], -``` - -By default, four rule inferrers are enabled: - -- **SometimesRuleInferrer** will add a `sometimes` rule when the property is optional -- **NullableRuleInferrer** will add a `nullable` rule when the property is nullable -- **RequiredRuleInferrer** will add a `required` rule when the property is not nullable -- **BuiltInTypesRuleInferrer** will add a rules which are based upon the built-in php types: - - An `int` or `float` type will add the `numeric` rule - - A `bool` type will add the `boolean` rule - - A `string` type will add the `string` rule - - A `array` type will add the `array` rule -- **AttributesRuleInferrer** will make sure that rule attributes we described above will also add their rules - -It is possible to write your rule inferrers. You can find more -information [here](/docs/laravel-data/v3/advanced-usage/creating-a-rule-inferrer). - -### Skipping validation - -Sometimes you don't want properties to be automatically validated, for instance when you're manually overwriting the -rules method like this: - -```php -class SongData extends Data -{ - public function __construct( - public string $name, - ) { - } - - public static function fromRequest(Request $request): static{ - return new self("{$request->input('first_name')} {$request->input('last_name')}") - } - - public static function rules(): array - { - return [ - 'first_name' => ['required', 'string'], - 'last_name' => ['required', 'string'], - ]; - } -} -``` - -When a request is being validated, the rules will look like this: - -```php -[ - 'name' => ['required', 'string'], - 'first_name' => ['required', 'string'], - 'last_name' => ['required', 'string'], -] -``` - -We know we never want to validate the `name` property since it won't be in the request payload, this can be done as -such: - -```php -class SongData extends Data -{ - public function __construct( - #[WithoutValidation] - public string $name, - ) { - } -} -``` - -Now the validation rules will look like this: - -```php -[ - 'first_name' => ['required', 'string'], - 'last_name' => ['required', 'string'], -] -``` - -### Overwriting the validator - -Before validating the values, it is possible to plugin into the validator. This can be done as such: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function withValidator(Validator $validator): void - { - $validator->after(function ($validator) { - $validator->errors()->add('field', 'Something is wrong with this field!'); - }); - } -} -``` - -### Overwriting messages - -It is possible to overwrite the error messages that will be returned when an error fails: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function messages(): array - { - return [ - 'title.required' => 'A title is required', - 'artist.required' => 'An artist is required', - ]; - } -} -``` - -### Overwriting attributes - -In the default Laravel validation rules, you can overwrite the name of the attribute as such: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function attributes(): array - { - return [ - 'title' => 'titel', - 'artist' => 'artiest', - ]; - } -} -``` - -### Overwriting other validation functionality - -Next to overwriting the validator, attributes and messages it is also possible to overwrite the following functionality. - -The redirect when a validation failed: - -```php -class SongData extends Data -{ - // ... - - public static function redirect(): string - { - return action(HomeController::class); - } -} -``` - -Or the route which will be used to redirect after a validation failed: - -```php -class SongData extends Data -{ - // ... - - public static function redirectRoute(): string - { - return 'home'; - } -} -``` - -Whether to stop validating on the first failure: - -```php -class SongData extends Data -{ - // ... - - public static function stopOnFirstFailure(): bool - { - return true; - } -} -``` - -The name of the error bag: - -```php -class SongData extends Data -{ - // ... - - public static function errorBag(): string - { - return 'never_gonna_give_an_error_up'; - } -} -``` - -### Using dependencies in overwritten functionality - -You can also provide dependencies to be injected in the overwritten validator functionality methods like `messages` -, `attributes`, `redirect`, `redirectRoute`, `stopOnFirstFailure`, `errorBag`: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function attributes( - ValidationAttributesLanguageRepository $validationAttributesLanguageRepository - ): array - { - return [ - 'title' => $validationAttributesLanguageRepository->get('title'), - 'artist' => $validationAttributesLanguageRepository->get('artist'), - ]; - } -} -``` - -## Authorizing a request - -Just like with Laravel requests, it is possible to authorize an action for certain people only: - -```php -class SongData extends Data -{ - public function __construct( - public string $title, - public string $artist, - ) { - } - - public static function authorize(): bool - { - return Auth::user()->name === 'Ruben'; - } -} -``` - -If the method returns `false`, then an `AuthorizationException` is thrown. ## Validating a collection of data objects: @@ -704,42 +106,3 @@ In this case the validation rules for `AlbumData` would look like this: 'songs.*.artist' => ['required', 'string'], ] ``` - -## Validating a data object without request - -It is also possible to validate values for a data object without using a request: - -```php -SongData::validate(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); -``` - -This will either throw a `ValidationException` or return a validated version of the payload. - -It is possible to return a data object when the payload is valid when calling: - -```php -SongData::validateAndCreate(['title' => 'Never gonna give you up', 'artist' => 'Rick Astley']); -``` - -## Retrieving validation rules for a data object - -You can retrieve the validation rules a data object will generate as such: - -```php -AlbumData::getValidationRules($payload); -``` - -This will produce the following array with rules: - -```php -[ - 'title' => ['required', 'string'], - 'songs' => ['required', 'array'], - 'songs.*.title' => ['required', 'string'], - 'songs.*.artist' => ['required', 'string'], -] -``` - -## Payload requirement - -We suggest always to provide a payload when generating validation rules. Because such a payload is used to determine which rules will be generated and which can be skipped. diff --git a/docs/as-a-resource/appending-properties.md b/docs/as-a-resource/appending-properties.md new file mode 100644 index 00000000..e6cae89b --- /dev/null +++ b/docs/as-a-resource/appending-properties.md @@ -0,0 +1,89 @@ +--- +title: Appending properties +weight: 4 +--- + +It is possible to add some extra properties to your data objects when they are transformed into a resource: + +```php +SongData::from(Song::first())->additional([ + 'year' => 1987, +]); +``` + +This will output the following array: + +```php +[ + 'name' => 'Never gonna give you up', + 'artist' => 'Rick Astley', + 'year' => 1987, +] +``` + +When using a closure, you have access to the underlying data object: + +```php +SongData::from(Song::first())->additional([ + 'slug' => fn(SongData $songData) => Str::slug($songData->title), +]); +``` + +Which produces the following array: + +```php +[ + 'name' => 'Never gonna give you up', + 'artist' => 'Rick Astley', + 'slug' => 'never-gonna-give-you-up', +] +``` + +It is also possible to add extra properties by overwriting the `with` method within your data object: + +```php +class SongData extends Data +{ + public function __construct( + public int $id, + public string $title, + public string $artist + ) { + } + + public static function fromModel(Song $song): self + { + return new self( + $song->id, + $song->title, + $song->artist + ); + } + + public function with() + { + return [ + 'endpoints' => [ + 'show' => action([SongsController::class, 'show'], $this->id), + 'edit' => action([SongsController::class, 'edit'], $this->id), + 'delete' => action([SongsController::class, 'delete'], $this->id), + ] + ]; + } +} +``` + +Now each transformed data object contains an `endpoints` key with all the endpoints for that data object: + +```php +[ + 'id' => 1, + 'name' => 'Never gonna give you up', + 'artist' => 'Rick Astley', + 'endpoints' => [ + 'show' => 'https://spatie.be/songs/1', + 'edit' => 'https://spatie.be/songs/1', + 'delete' => 'https://spatie.be/songs/1', + ], +] +``` diff --git a/docs/as-a-resource/from-data-to-array.md b/docs/as-a-resource/from-data-to-array.md new file mode 100644 index 00000000..d58449a5 --- /dev/null +++ b/docs/as-a-resource/from-data-to-array.md @@ -0,0 +1,132 @@ +--- +title: From data to array +weight: 1 +--- + +A data object can automatically be transformed into an array as such: + +```php +SongData::from(Song::first())->toArray(); +``` + +Which will output the following array: + +```php +[ + 'name' => 'Never gonna give you up', + 'artist' => 'Rick Astley' +] +``` + +You can manually transform a data object to JSON: + +```php +SongData::from(Song::first())->toJson(); +``` + +Or transform a data object to an array: + +```php +SongData::from(Song::first())->toArray(); +``` + +By default, calling `toArray` on a data object will transform all properties to an array. This means that nested data objects and collections of data objects will also be transformed to arrays. Other complex types like `Carbon`, `DateTime`, `Enums`, ... will be transformed into a string. We'll see in the [transformers](/docs/laravel-data/v4/as-a-resource/transformers) section how to configure and customize this behavior. + +If you only want to transform a data object to an array without transforming the properties, you can call the `all` method: + +```php +SongData::from(Song::first())->all(); +``` + +## Using collections + +Here's how to create a collection of data objects: + +```php +SongData::collect(Song::all()); +``` + +A collection can be transformed to array: + +```php +SongData::collect(Song::all())->toArray(); +``` + +Which will output the following array: + +```php +[ + [ + "name": "Never Gonna Give You Up", + "artist": "Rick Astley" + ], + [ + "name": "Giving Up on Love", + "artist": "Rick Astley" + ] +] +``` + +## Nesting + +It is possible to nest data objects. + +```php +class UserData extends Data +{ + public function __construct( + public string $title, + public string $email, + public SongData $favorite_song, + ) { + } + + public static function fromModel(User $user): self + { + return new self( + $user->title, + $user->email, + SongData::from($user->favorite_song) + ); + } +} +``` + +When transformed to an array, this will look like the following: + +```php +[ + "name": "Ruben", + "email": "ruben@spatie.be", + "favorite_song": [ + "name" : "Never Gonna Give You Up", + "artist" : "Rick Astley" + ] +] +``` + +You can also nest a collection of data objects: + +```php +class AlbumData extends Data +{ + /** + * @param Collection $songs + */ + public function __construct( + public string $title, + public array $songs, + ) { + } + + public static function fromModel(Album $album): self + { + return new self( + $album->title, + SongData::collect($album->songs) + ); + } +} +``` + +As always, don't forget to type collections of data objects by annotation or the `DataCollectionOf` attribute, this is essential to transform these collections correctly. diff --git a/docs/as-a-resource/from-data-to-resource.md b/docs/as-a-resource/from-data-to-resource.md index e5f53cb8..efbe389f 100644 --- a/docs/as-a-resource/from-data-to-resource.md +++ b/docs/as-a-resource/from-data-to-resource.md @@ -1,6 +1,6 @@ --- title: From data to resource -weight: 1 +weight: 2 --- A data object will automatically be transformed to a JSON response when returned in a controller: @@ -24,164 +24,15 @@ The JSON then will look like this: } ``` -You can manually transform a data object to JSON: +### Collections -```php -SongData::from(Song::first())->toJson(); -``` - -Or transform a data object to an array: - -```php -SongData::from(Song::first())->toArray(); -``` - -## Transforming empty objects - -When creating a new model, you probably want to provide a blueprint to the frontend with the required data to create a model. For example: - -```json -{ - "name": null, - "artist": null -} -``` - -You could make each property of the data object nullable like this: - -```php -class SongData extends Data -{ - public function __construct( - public ?string $title, - public ?string $artist, - ) { - } - - // ... -} -``` - -This approach would work, but as soon as the model is created, the properties won't be `null`, which doesn't follow our data model. So it is considered a bad practice. - -That's why in such cases, you can return an empty representation of the data object: - -```php -class SongsController -{ - public function create(): array - { - return SongData::empty(); - } -} -``` - -Which will output the following JSON: - -```json -{ - "name": null, - "artist": null -} -``` - -The `empty` method on a data object will return an array with default empty values for the properties in the data object. - -It is possible to change the default values within this array by providing them in the constructor of the data object: - - ```php - class SongData extends Data -{ - public function __construct( - public string $title = 'Title of the song here', - public string $artist = "An artist", - ) { - } - - // ... -} - ``` - -Now when we call `empty`, our JSON looks like this: - -```json -{ - "name": "Title of the song here", - "artist": "An artist" -} -``` - -You can also pass defaults within the `empty` call: - -```php -SongData::empty([ - 'name' => 'Title of the song here', - 'artist' => 'An artist' -]); -``` - -## Mapping property names - -Sometimes you might want to change the name of a property, with attributes this is possible: - -```php -class ContractData extends Data -{ - public function __construct( - public string $name, - #[MapOutputName('record_company')] - public string $recordCompany, - ) { - } -} -``` - -Now our JSON looks like this: - -```json -{ - "name": "Rick Astley", - "record_company": "RCA Records" -} -``` - -Changing all property names in a data object to snake_case as output data can be done as such: - -```php -#[MapOutputName(SnakeCaseMapper::class)] -class ContractData extends Data -{ - public function __construct( - public string $name, - public string $recordCompany, - ) { - } -} -``` - -You can also use the `MapName` attribute when you want to combine input and output property name mapping: +Returning a data collection from the controller like this: ```php -#[MapName(SnakeCaseMapper::class)] -class ContractData extends Data -{ - public function __construct( - public string $name, - public string $recordCompany, - ) { - } -} +SongData::collect(Song::all()); ``` -## Using collections - -Here's how to create a collection of data objects: - -```php -SongData::collection(Song::all()); -``` - -A collection can be returned in a controller and will automatically be transformed to JSON: +Will return a collection automatically transformed to JSON: ```json [ @@ -196,16 +47,12 @@ A collection can be returned in a controller and will automatically be transform ] ``` -You can also transform a collection of data objects into an array: - -```php -SongData::collection(Song::all())->toArray(); -``` +### Paginators -It is also possible to provide a paginated collection: +It is also possible to provide a paginator: ```php -SongData::collection(Song::paginate()); +SongData::collect(Song::paginate()); ``` The data object is smart enough to create a paginated response from this with links to the next, previous, last, ... pages: @@ -238,174 +85,92 @@ The data object is smart enough to create a paginated response from this with li } ``` -It is possible to change data objects in a collection: - -```php -$allSongs = Song::all(); - -SongData::collection($allSongs)->through(function(SongData $song){ - $song->artist = 'Abba'; - - return $song; -}); -``` - -You can filter non-paginated collections: -```php -SongData::collection($allSongs)->filter( - fn(SongData $song) => $song->artist === 'Rick Astley' -); -``` - -## Nesting - -It is possible to nest data objects. - -```php -class UserData extends Data -{ - public function __construct( - public string $title, - public string $email, - public SongData $favorite_song, - ) { - } - - public static function fromModel(User $user): self - { - return new self( - $user->title, - $user->email, - SongData::from($user->favorite_song) - ); - } -} -``` +## Transforming empty objects -When transformed to JSON, this will look like the following: +When creating a new model, you probably want to provide a blueprint to the frontend with the required data to create a model. For example: ```json { - "name": "Ruben", - "email": "ruben@spatie.be", - "favorite_song": { - "name" : "Never Gonna Give You Up", - "artist" : "Rick Astley" - } + "name": null, + "artist": null } ``` -You can also nest a collection of resources: +You could make each property of the data object nullable like this: ```php -class AlbumData extends Data +class SongData extends Data { public function __construct( - public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, + public ?string $title, + public ?string $artist, ) { } - public static function fromModel(Album $album): self - { - return new self( - $album->title, - SongData::collection($album->songs) - ); - } + // ... } ``` -We're using a `DataCollection` type here in the data object definition. It would be best always to use a `DataCollection` type when nesting a collection of data objects. The package requires this for internal state management. - -## Appending properties +This approach would work, but as soon as the model is created, the properties won't be `null`, which doesn't follow our data model. So it is considered a bad practice. -It is possible to add some extra properties to your data objects when they are transformed into a resource: +That's why in such cases, you can return an empty representation of the data object: ```php -SongData::from(Song::first())->additional([ - 'year' => 1987, -]); -``` - -This will output the following JSON: - -```json +class SongsController { - "name": "Never gonna give you up", - "artist": "Rick Astley", - "year": 1987 + public function create(): array + { + return SongData::empty(); + } } ``` -When using a closure, you have access to the underlying data object: - -```php -SongData::from(Song::first())->additional([ - 'slug' => fn(SongData $songData) => Str::slug($songData->title), -]); -``` - -Which produces the following: +Which will output the following JSON: ```json { - "name": "Never gonna give you up", - "artist": "Rick Astley", - "slug": "never-gonna-give-you-up" + "name": null, + "artist": null } ``` -It is also possible to add extra properties by overwriting the `with` method within your data object: +The `empty` method on a data object will return an array with default empty values for the properties in the data object. -```php -class SongData extends Data +It is possible to change the default values within this array by providing them in the constructor of the data object: + + ```php + class SongData extends Data { public function __construct( - public int $id, - public string $title, - public string $artist + public string $title = 'Title of the song here', + public string $artist = "An artist", ) { } - - public static function fromModel(Song $song): self - { - return new self( - $song->id, - $song->title, - $song->artist - ); - } - public function with(){ - return [ - 'endpoints' => [ - 'show' => action([SongsController::class, 'show'], $this->id), - 'edit' => action([SongsController::class, 'edit'], $this->id), - 'delete' => action([SongsController::class, 'delete'], $this->id), - ] - ]; - } + // ... } -``` + ``` -Now each transformed data object contains an `endpoints` key with all the endpoints for that data object: +Now when we call `empty`, our JSON looks like this: ```json { - "id": 1, - "name": "Never gonna give you up", - "artist": "Rick Astley", - "endpoints": { - "show": "https://spatie.be/songs/1", - "edit": "https://spatie.be/songs/1", - "delete": "https://spatie.be/songs/1" - } + "name": "Title of the song here", + "artist": "An artist" } +``` + +You can also pass defaults within the `empty` call: + +```php +SongData::empty([ + 'name' => 'Title of the song here', + 'artist' => 'An artist' +]); ``` + ## Response status code -When a resource is being returned from a controller, the status code of the response will automatically be set to `201 CREATED` when Laravel data detectes that the request's method is `POST`. In all other cases, `200 OK` will be returned. +When a resource is being returned from a controller, the status code of the response will automatically be set to `201 CREATED` when Laravel data detects that the request's method is `POST`. In all other cases, `200 OK` will be returned. diff --git a/docs/as-a-resource/lazy-properties.md b/docs/as-a-resource/lazy-properties.md index 32b7bf2a..d812b548 100644 --- a/docs/as-a-resource/lazy-properties.md +++ b/docs/as-a-resource/lazy-properties.md @@ -1,6 +1,6 @@ --- title: Including and excluding properties -weight: 2 +weight: 6 --- Sometimes you don't want all the properties included when transforming a data object to an array, for example: diff --git a/docs/as-a-resource/mapping-property-names.md b/docs/as-a-resource/mapping-property-names.md new file mode 100644 index 00000000..b656c0a2 --- /dev/null +++ b/docs/as-a-resource/mapping-property-names.md @@ -0,0 +1,73 @@ +--- +title: Mapping property names +weight: 3 +--- + +Sometimes you might want to change the name of a property in the transformed payload, with attributes this is possible: + +```php +class ContractData extends Data +{ + public function __construct( + public string $name, + #[MapOutputName('record_company')] + public string $recordCompany, + ) { + } +} +``` + +Now our array looks like this: + +```php +[ + 'name' => 'Rick Astley', + 'record_company' => 'RCA Records', +] +``` + +Changing all property names in a data object to snake_case as output data can be done as such: + +```php +#[MapOutputName(SnakeCaseMapper::class)] +class ContractData extends Data +{ + public function __construct( + public string $name, + public string $recordCompany, + ) { + } +} +``` + +You can also use the `MapName` attribute when you want to combine input and output property name mapping: + +```php +#[MapName(SnakeCaseMapper::class)] +class ContractData extends Data +{ + public function __construct( + public string $name, + public string $recordCompany, + ) { + } +} +``` + +You can now create a data object as such: + +```php +$contract = new ContractData( + name: 'Rick Astley', + record_company: 'RCA Records', +); +``` + +And a transformed version of the data object will look like this: + +```php +[ + 'name' => 'Rick Astley', + 'record_company' => 'RCA Records', +] +``` diff --git a/docs/as-a-resource/transformers.md b/docs/as-a-resource/transformers.md index f97e08f9..988184bf 100644 --- a/docs/as-a-resource/transformers.md +++ b/docs/as-a-resource/transformers.md @@ -1,6 +1,6 @@ --- title: Transforming data -weight: 4 +weight: 7 --- Each property of a data object should be transformed into a usable type to communicate via JSON. @@ -43,7 +43,7 @@ class ArtistData extends Data{ Next to a `DateTimeInterfaceTransformer` the package also ships with an `ArrayableTransformer` that transforms an `Arrayable` object to an array. -It is possible to create transformers for your specific types. You can find more info [here](/docs/laravel-data/v3/advanced-usage/creating-a-transformer). +It is possible to create transformers for your specific types. You can find more info [here](/docs/laravel-data/v4/advanced-usage/creating-a-transformer). ## Global transformers diff --git a/docs/as-a-resource/wrapping.md b/docs/as-a-resource/wrapping.md index 1645e08c..d0c42755 100644 --- a/docs/as-a-resource/wrapping.md +++ b/docs/as-a-resource/wrapping.md @@ -1,6 +1,6 @@ --- title: Wrapping -weight: 3 +weight: 5 --- By default, when a data object is transformed into JSON in your controller it looks like this: @@ -61,7 +61,7 @@ Or you can set a global wrap key inside the `data.php` config file: Collections can be wrapped just like data objects: ```php -SongData::collection(Song::all())->wrap('data'); +SongData::collect(Song::all())->wrap('data'); ``` The JSON will now look like this: @@ -84,7 +84,7 @@ The JSON will now look like this: It is possible to set the data key in paginated collections: ```php -SongData::collection(Song::paginate())->wrap('paginated_data'); +SongData::collect(Song::paginate())->wrap('paginated_data'); ``` Which will let the JSON look like this: @@ -160,7 +160,7 @@ UserData::from(User::first())->wrap('data'); A data collection inside a data object WILL get wrapped when a wrapping key is set: ```php -use Spatie\LaravelData\Attributes\DataCollectionOf; +use Spatie\LaravelData\Attributes\DataCollectionOf;use Spatie\LaravelData\DataCollection; class AlbumData extends Data { @@ -175,7 +175,7 @@ class AlbumData extends Data { return new self( $album->title, - SongData::collection($album->songs)->wrap('data') + SongData::collect($album->songs, DataCollection::class)->wrap('data') ); } } diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index a1bb1160..284b3218 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -5,7 +5,7 @@ weight: 1 In this quickstart, we'll guide you through the most important functionalities of the package and how to use them. -First, you should [install the package](/docs/laravel-data/v3/installation-setup). +First, you should [install the package](/docs/laravel-data/v4/installation-setup). We will create a blog with different posts, so let's start with the `PostData` object. A post has a title, some content, a status and a date when it was published: @@ -189,7 +189,7 @@ As you can see, we're missing the `date` rule on the `published_at` property. By - `array` when a property type is `array` - `enum:*` when a property type is a native enum -You can read more about the process of automated rule generation [here](/docs/laravel-data/v3/as-a-data-transfer-object/request-to-data-object#content-automatically-inferring-rules-for-properties-1). +You can read more about the process of automated rule generation [here](/docs/laravel-data/v4/as-a-data-transfer-object/request-to-data-object#content-automatically-inferring-rules-for-properties-1). We can easily add the date rule by using an attribute to our data object: @@ -232,7 +232,7 @@ array:4 [ ] ``` -There are [tons](/docs/laravel-data/v3/advanced-usage/validation-attributes) of validation rule attributes you can add to data properties. There's still much more you can do with validating data objects. Read more about it [here](/docs/laravel-data/v3/as-a-data-transfer-object/request-to-data-object#validating-a-request). +There are [tons](/docs/laravel-data/v4/advanced-usage/validation-attributes) of validation rule attributes you can add to data properties. There's still much more you can do with validating data objects. Read more about it [here](/docs/laravel-data/v4/as-a-data-transfer-object/request-to-data-object#validating-a-request). Tip: By default, when creating a data object in a non request context, no validation is executed: @@ -275,7 +275,7 @@ It is possible to define casts within the `data.php` config file. By default, th This code means that if a class property is of type `DateTime`, `Carbon`, `CarbonImmutable`, ... it will be automatically cast. -You can create your own casts; read more about it [here](/docs/laravel-data/v3/advanced-usage/creating-a-cast). +You can create your own casts; read more about it [here](/docs/laravel-data/v4/advanced-usage/creating-a-cast). ### Local casts @@ -310,7 +310,7 @@ use Str; class ImageCast implements Cast { - public function cast(DataProperty $property, mixed $value, array $context): Image + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): Image { // Scenario A if ($value instanceof UploadedFile) { @@ -356,7 +356,7 @@ class PostData extends Data } ``` -You can read more about casting [here](/docs/laravel-data/v3/as-a-data-transfer-object/casts). +You can read more about casting [here](/docs/laravel-data/v4/as-a-data-transfer-object/casts). ## Customizing the creation of a data object @@ -406,44 +406,54 @@ class PostData extends Data ``` Magic creation methods allow you to create data objects from any type by passing them to the `from` method of a data -object, you can read more about it [here](/laravel-data/v3/as-a-data-transfer-object/creating-a-data-object#magical-creation). +object, you can read more about it [here](/laravel-data/v4/as-a-data-transfer-object/creating-a-data-object#magical-creation). It can be convenient to transform more complex models than our `Post` into data objects because you can decide how a model would be mapped onto a data object. -## Nesting data objects and collections +## Nesting data objects and arrays of data objects -Now that we have a fully functional post-data object. We're going to create a new data object, `AuthorData`, that will store the name of an author and a collection of posts the author wrote: +Now that we have a fully functional post-data object. We're going to create a new data object, `AuthorData`, that will store the name of an author and an array of posts the author wrote: ```php use Spatie\LaravelData\Attributes\DataCollectionOf; class AuthorData extends Data { + /** + * @param array $posts + */ public function __construct( public string $name, - #[DataCollectionOf(PostData::class)] - public DataCollection $posts + public array $posts ) { } } ``` -Instead of using an array to store all the posts, we use a `DataCollection .`This will be very useful later on! The package always needs to know what type of data is stored in a `DataCollection`, so we use the `DataCollectionOf` attribute to tell it is a collection of `PostData` objects. +Notice that we've typed the `$posts` property as an array of `PostData` objects using a docblock. This will be very useful later on! The package always needs to know what type of data objects are stored in an array. Off course, when you're storing other types then data objects this is not required but recommended. We can now create an author object as such: ```php new AuthorData( 'Ruben Van Assche', - PostData::collection([ - new PostData('Hello laravel-data', 'This is an introduction post for the new package,' PostStatus::draft, null, null), - new PostData('What is a data object', 'How does it work?', PostStatus::draft, null, null), + PostData::collect([ + [ + 'title' => 'Hello laravel-data', + 'content' => 'This is an introduction post for the new package', + 'status' => PostStatus::draft, + ], + [ + 'title' => 'What is a data object', + 'content' => 'How does it work', + 'status' => PostStatus::published, + ], ]) ); ``` -As you can see, the `collection` method can create a new `DataCollection` of the `PostData` object. +As you can see, the `collect` method can create an array of the `PostData` objects. But there's another way; thankfully, our `from` method makes this process even more straightforward: @@ -465,9 +475,7 @@ AuthorData::from([ ]); ``` -The data object is smart enough to convert an array of posts into a data collection of post data. Mapping data coming from the front end was never that easy! - -You can do a lot more with data collections. Read more about it [here](/docs/laravel-data/v3/as-a-data-transfer-object/collections). +The data object is smart enough to convert an array of posts into an array of post data. Mapping data coming from the front end was never that easy! ### Nesting objects @@ -674,12 +682,12 @@ This `DateTimeInterfaceTransformer` is registered in the `data.php` config file ], ``` -Rember the image object we created earlier; we stored a file size and filename in the object. But that could be more useful; let's provide the URL to the file when transforming the object. Just like casts, transformers also can be local. Let's implement one for `Image`: +Remember the image object we created earlier; we stored a file size and filename in the object. But that could be more useful; let's provide the URL to the file when transforming the object. Just like casts, transformers also can be local. Let's implement one for `Image`: ```php class ImageTransformer implements Transformer { - public function transform(DataProperty $property, mixed $value): string + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string { if (! $value instanceof Image) { throw new Exception("Not an image"); @@ -739,7 +747,7 @@ Which leads to the following JSON: } ``` -You can read more about transformers [here](/docs/laravel-data/v3/as-a-resource/transformers). +You can read more about transformers [here](/docs/laravel-data/v4/as-a-resource/transformers). ## Generating a blueprint @@ -842,10 +850,12 @@ This functionality can be achieved with lazy properties. Lazy properties are onl ```php class AuthorData extends Data { + /** + * @param Collection|Lazy $posts + */ public function __construct( public string $name, - #[DataCollectionOf(PostData::class)] - public DataCollection|Lazy $posts + public Collection|Lazy $posts ) { } @@ -853,7 +863,7 @@ class AuthorData extends Data { return new self( $author->name, - Lazy::create(fn() => PostData::collection($author->posts)) + Lazy::create(fn() => PostData::collect($author->posts)) ); } } @@ -1005,19 +1015,19 @@ You can do quite a lot with lazy properties like including them: - when they are requested in the URL query - by default, with an option to exclude them -And a lot more. You can read all about it [here](/docs/laravel-data/v3/as-a-resource/lazy-properties). +And a lot more. You can read all about it [here](/docs/laravel-data/v4/as-a-resource/lazy-properties). ## Conclusion So that's it, a quick overview of this package. We barely scratched the surface of what's possible with the package. There's still a lot more you can do with data objects like: -- [casting](/docs/laravel-data/v3/advanced-usage/eloquent-casting) them into Eloquent models -- [transforming](/docs/laravel-data/v3/advanced-usage/typescript) the structure to typescript -- [working](/docs/laravel-data/v3/as-a-data-transfer-object/collections) with `DataCollections` -- [optional properties](/docs/laravel-data/v3/as-a-data-transfer-object/optional-properties) not always required when creating a data object -- [wrapping](/docs/laravel-data/v3/as-a-resource/wrapping) transformed data into keys -- [mapping](/docs/laravel-data/v3/as-a-data-transfer-object/creating-a-data-object#content-mapping-property-names) property names when creating or transforming a data object -- [appending](/docs/laravel-data/v3/as-a-resource/from-data-to-resource#content-appending-properties) extra data -- [including](/docs/laravel-data/v3/as-a-resource/lazy-properties#content-using-query-strings) properties using the URL query string -- [inertia](https://spatie.be/docs/laravel-data/v3/advanced-usage/use-with-inertia) support for lazy properties +- [casting](/docs/laravel-data/v4/advanced-usage/eloquent-casting) them into Eloquent models +- [transforming](/docs/laravel-data/v4/advanced-usage/typescript) the structure to typescript +- [working](/docs/laravel-data/v4/as-a-data-transfer-object/collections) with `DataCollections` +- [optional properties](/docs/laravel-data/v4/as-a-data-transfer-object/optional-properties) not always required when creating a data object +- [wrapping](/docs/laravel-data/v4/as-a-resource/wrapping) transformed data into keys +- [mapping](/docs/laravel-data/v4/as-a-data-transfer-object/mapping-property-names) property names when creating or transforming a data object +- [appending](/docs/laravel-data/v4/as-a-resource/from-data-to-resource#content-appending-properties) extra data +- [including](/docs/laravel-data/v4/as-a-resource/lazy-properties#content-using-query-strings) properties using the URL query string +- [inertia](https://spatie.be/docs/laravel-data/v4/advanced-usage/use-with-inertia) support for lazy properties - and so much more ... you'll find all the information here in the docs diff --git a/docs/installation-setup.md b/docs/installation-setup.md index e35eeba2..7c03dc9d 100644 --- a/docs/installation-setup.md +++ b/docs/installation-setup.md @@ -19,14 +19,14 @@ This is the contents of the published config file: ```php return [ - /* + /** * The package will use this format when working with dates. If this option * is an array, it will try to convert from the first format that works, * and will serialize dates using the first format from the array. */ 'date_format' => DATE_ATOM, - /* + /** * Global transformers will take complex types and transform them into simple * types. */ @@ -36,7 +36,7 @@ return [ BackedEnum::class => Spatie\LaravelData\Transformers\EnumTransformer::class, ], - /* + /** * Global casts will cast values into complex types when creating a data * object from simple types. */ @@ -45,7 +45,7 @@ return [ BackedEnum::class => Spatie\LaravelData\Casts\EnumCast::class, ], - /* + /** * Rule inferrers can be configured here. They will automatically add * validation rules to properties of a data object based upon * the type of the property. @@ -65,13 +65,14 @@ return [ */ 'normalizers' => [ Spatie\LaravelData\Normalizers\ModelNormalizer::class, + // Spatie\LaravelData\Normalizers\FormRequestNormalizer::class, Spatie\LaravelData\Normalizers\ArrayableNormalizer::class, Spatie\LaravelData\Normalizers\ObjectNormalizer::class, Spatie\LaravelData\Normalizers\ArrayNormalizer::class, Spatie\LaravelData\Normalizers\JsonNormalizer::class, ], - /* + /** * Data objects can be wrapped into a key like 'data' when used as a resource, * this key can be set globally here for all data objects. You can pass in * `null` if you want to disable wrapping. @@ -85,5 +86,37 @@ return [ * which will only enable the caster locally. */ 'var_dumper_caster_mode' => 'development', + + /** + * It is possible to skip the PHP reflection analysis of data objects + * when running in production. This will speed up the package. You + * can configure where data objects are stored and which cache + * store should be used. + */ + 'structure_caching' => [ + 'directories' => [app_path('Data')], + 'cache' => [ + 'store' => env('CACHE_DRIVER', 'file'), + 'prefix' => 'laravel-data', + ], + 'reflection_discovery' => [ + 'enabled' => true, + 'base_path' => base_path(), + 'root_namespace' => null, + ], + ], + + /** + * A data object can be validated when created using a factory or when calling the from + * method. By default, only when a request is passed the data is being validated. This + * behaviour can be changed to always validate or to completely disable validation. + */ + 'validation_strategy' => \Spatie\LaravelData\Support\Creation\ValidationStrategy::OnlyRequests->value, + + /** + * When using an invalid include, exclude, only or except partial, the package will + * throw an + */ + 'ignore_invalid_partials' => false, ]; ``` diff --git a/docs/third-party-packages.md b/docs/third-party-packages.md index 8ef7ca7c..425fc947 100644 --- a/docs/third-party-packages.md +++ b/docs/third-party-packages.md @@ -5,6 +5,7 @@ weight: 5 Some community members created packages that extend the functionality of Laravel Data. Here's a list of them: +- [laravel-typescript-transformer](https://github.com/spatie/laravel-typescript-transformer) - [laravel-data-openapi-generator](https://github.com/xolvionl/laravel-data-openapi-generator) Created a package yourself that you want to add to this list? Send us a PR! diff --git a/docs/validation/auto-rule-inferring.md b/docs/validation/auto-rule-inferring.md index 516c5fed..50cadefe 100644 --- a/docs/validation/auto-rule-inferring.md +++ b/docs/validation/auto-rule-inferring.md @@ -57,4 +57,4 @@ By default, five rule inferrers are enabled: - A `array` type will add the `array` rule - **AttributesRuleInferrer** will make sure that rule attributes we described above will also add their rules -It is possible to write your rule inferrers. You can find more information [here](/docs/laravel-data/v3/advanced-usage/creating-a-rule-inferrer). +It is possible to write your rule inferrers. You can find more information [here](/docs/laravel-data/v4/advanced-usage/creating-a-rule-inferrer). diff --git a/docs/validation/introduction.md b/docs/validation/introduction.md index 99aad577..cf2c5f3d 100644 --- a/docs/validation/introduction.md +++ b/docs/validation/introduction.md @@ -3,7 +3,7 @@ title: Introduction weight: 1 --- -Laravel data allows you to create data objects from all sorts of data. One of the most common ways to create a data object is from a request and the data from a request cannot always be trusted. +Laravel data, allows you to create data objects from all sorts of data. One of the most common ways to create a data object is from a request and the data from a request cannot always be trusted. That's why it is possible to validate the data before creating the data object. You can validate requests but also arrays and other structures. @@ -13,7 +13,7 @@ The package will try to automatically infer validation rules from the data objec Validation is probably one of the coolest features of this package, but it is also the most complex one. We'll try to make it as straightforward as possible to validate data but in the end the Laravel validator was not written to be used in this way. So there are some limitations and quirks you should be aware of. -In some cases it might be easier to just create a custom request class with validation rules and then call `toArray` on the request to create a data object than trying to validate the data with this package. +In a few cases it might be easier to just create a custom request class with validation rules and then call `toArray` on the request to create a data object than trying to validate the data with this package. ## When does validation happen? @@ -42,6 +42,22 @@ SongData::validateAndCreate( ); // returns a SongData object ``` +### Validate everything + +It is possible to validate all payloads injected or passed to the `from` method by setting the `validation_strategy` config option to `Always`: + +```php +'validation_strategy' => \Spatie\LaravelData\Support\Creation\ValidationStrategy::Always->value, +``` + +Completely disabling validation can be done by setting the `validation_strategy` config option to `Disabled`: + +```php +'validation_strategy' => \Spatie\LaravelData\Support\Creation\ValidationStrategy::Disabled->value, +``` + +If you require a more fine-grained control over when validation should happen, you can use [data factories](/docs/laravel-data/v4//as-a-data-transfer-object/factories.md) to manually specify the validation strategy. + ## A quick glance at the validation functionality We've got a lot of documentation about validation and we suggest you read it all, but if you want to get a quick glance at the validation functionality, here's a quick overview: @@ -71,7 +87,7 @@ The package will generate the following validation rules: ] ``` -The package follows an algorithm to infer rules from the data object, you can read more about it [here](/docs/laravel-data/v3/validation/auto-rule-inferring). +The package follows an algorithm to infer rules from the data object, you can read more about it [here](/docs/laravel-data/v4/validation/auto-rule-inferring). ### Validation attributes @@ -91,7 +107,7 @@ class SongData extends Data When you provide an artist with a length of more than 20 characters, the validation will fail. -There's a complete [chapter](/docs/laravel-data/v3/validation/using-attributes) dedicated to validation attributes. +There's a complete [chapter](/docs/laravel-data/v4/validation/using-attributes) dedicated to validation attributes. ### Manual rules @@ -116,7 +132,7 @@ class SongData extends Data } ``` -You can read more about manual rules in its [dedicated chapter](/docs/laravel-data/v3/validation/manual-rules). +You can read more about manual rules in its [dedicated chapter](/docs/laravel-data/v4/validation/manual-rules). ### Using the container @@ -131,7 +147,7 @@ If the request contains data that is not compatible with the data object, a vali ### Working with the validator -We provide a few points where you can hook into the validation process. You can read more about it in the [dedicated chapter](/docs/laravel-data/v3/validation/working-with-the-validator). +We provide a few points where you can hook into the validation process. You can read more about it in the [dedicated chapter](/docs/laravel-data/v4/validation/working-with-the-validator). It is for example to: @@ -191,7 +207,7 @@ The validation rules for this class will be: ] ``` -There are a few quirky things to keep in mind when working with nested data objects, you can read all about it [here](/docs/laravel-data/v3/validation/nesting-data). +There are a few quirky things to keep in mind when working with nested data objects, you can read all about it [here](/docs/laravel-data/v4/validation/nesting-data). ## Validation of nested data collections @@ -225,7 +241,7 @@ In this case the validation rules for `AlbumData` would look like this: ] ``` -More info about nested data collections can be found [here](/docs/laravel-data/v3/validation/nesting-data). +More info about nested data collections can be found [here](/docs/laravel-data/v4/validation/nesting-data). ## Default values diff --git a/docs/validation/nesting-data.md b/docs/validation/nesting-data.md index 6f5ba152..b7e28a84 100644 --- a/docs/validation/nesting-data.md +++ b/docs/validation/nesting-data.md @@ -3,4 +3,127 @@ title: Nesting Data weight: 6 --- -Work in progress +A data object can contain other data objects or collections of data objects. The package will make sure that also for these data objects validation rules will be generated. + +When we again have a look at the data object from the [nesting](/docs/laravel-data/v4/as-a-data-transfer-object/nesting) section: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $title, + public ArtistData $artist, + ) { + } +} +``` + +The validation rules for this class would be: + +```php +[ + 'title' => ['required', 'string'], + 'artist' => ['array'], + 'artist.name' => ['required', 'string'], + 'artist.age' => ['required', 'integer'], +] +``` + +## Validating a nested collection of data objects + +When validating a data object like this + +```php +class AlbumData extends Data +{ + /** + * @param array $songs + */ + public function __construct( + public string $title, + public array $songs, + ) { + } +} +``` + +In this case the validation rules for `AlbumData` would look like this: + +```php +[ + 'title' => ['required', 'string'], + 'songs' => ['present', 'array', new NestedRules()], +] +``` + +The `NestedRules` class is a Laravel validation rule that will validate each item within the collection for the rules defined on the data class for that collection. + +## Nullable and Optional nested data + +If we make the nested data object nullable , the validation rules will change depending on the payload provided: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $title, + public ?ArtistData $artist, + ) { + } +} +``` + +If no value for the nested object key was provided or the value is `null`, the validation rules will be: + +```php +[ + 'title' => ['required', 'string'], + 'artist' => ['nullable'], +] +``` + +If however a value was provided (even an empty array), the validation rules will be: + +```php +[ + 'title' => ['required', 'string'], + 'artist' => ['array'], + 'artist.name' => ['required', 'string'], + 'artist.age' => ['required', 'integer'], +] +``` + +The same happens when a property is made optional: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $title, + public ArtistData $artist, + ) { + } +} +``` + +There's a small difference though compared against nullable, when no value was provided for the nested object key, the validation rules will be: + +```php +[ + 'title' => ['required', 'string'], + 'artist' => ['present', 'array', new NestedRules()], +] +``` + +However, when a value was provided (even an empty array or null), the validation rules will be: + +```php +[ + 'title' => ['required', 'string'], + 'artist' => ['array'], + 'artist.name' => ['required', 'string'], + 'artist.age' => ['required', 'integer'], +] +``` + +We've written a [blog post](https://flareapp.io/blog/fixing-nested-validation-in-laravel) on the reasoning behind these variable validation rules based upon payload. And they are also the reason why calling `getValidationRules` on a data object always requires a payload to be provided. diff --git a/docs/validation/skipping-validation.md b/docs/validation/skipping-validation.md index c8bb52fa..b5e2c622 100644 --- a/docs/validation/skipping-validation.md +++ b/docs/validation/skipping-validation.md @@ -60,3 +60,7 @@ Now the validation rules will look like this: 'last_name' => ['required', 'string'], ] ``` + +## Skipping validation for all properties + +By using [data factories](/docs/laravel-data/v4/as-a-data-transfer-object/factories.md) or setting the `validation_strategy` in the `data.php` config you can skip validation for all properties of a data class. diff --git a/docs/validation/using-validation-attributes.md b/docs/validation/using-validation-attributes.md index 5e437227..d8f2741d 100644 --- a/docs/validation/using-validation-attributes.md +++ b/docs/validation/using-validation-attributes.md @@ -29,8 +29,7 @@ So it is not required to add the `required` and `string` rule, these will be add ] ``` -For each Laravel validation rule we've got a matching validation attribute, you can find a list of them [here](/docs/laravel-data/v3/advanced-usage/using-attributes). - +For each Laravel validation rule we've got a matching validation attribute, you can find a list of them [here](/docs/laravel-data/v4/advanced-usage/using-attributes). ## Referencing route parameters diff --git a/src/DataPipes/ValidatePropertiesDataPipe.php b/src/DataPipes/ValidatePropertiesDataPipe.php index b1baa4a0..07ea8c10 100644 --- a/src/DataPipes/ValidatePropertiesDataPipe.php +++ b/src/DataPipes/ValidatePropertiesDataPipe.php @@ -4,7 +4,7 @@ use Illuminate\Http\Request; use Spatie\LaravelData\Support\Creation\CreationContext; -use Spatie\LaravelData\Support\Creation\ValidationType; +use Spatie\LaravelData\Support\Creation\ValidationStrategy; use Spatie\LaravelData\Support\DataClass; class ValidatePropertiesDataPipe implements DataPipe @@ -15,11 +15,11 @@ public function handle( array $properties, CreationContext $creationContext ): array { - if ($creationContext->validationType === ValidationType::Disabled) { + if ($creationContext->validationStrategy === ValidationStrategy::Disabled) { return $properties; } - if ($creationContext->validationType === ValidationType::OnlyRequests && ! $payload instanceof Request) { + if ($creationContext->validationStrategy === ValidationStrategy::OnlyRequests && ! $payload instanceof Request) { return $properties; } diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php index 255fe41a..030b6709 100644 --- a/src/Support/Creation/CreationContext.php +++ b/src/Support/Creation/CreationContext.php @@ -29,7 +29,7 @@ class CreationContext */ public function __construct( public string $dataClass, - public readonly ValidationType $validationType, + public readonly ValidationStrategy $validationStrategy, public readonly bool $mapPropertyNames, public readonly bool $withoutMagicalCreation, public readonly ?array $ignoredMagicalMethods, diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index ed148485..f630a409 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -30,7 +30,7 @@ class CreationContextFactory */ public function __construct( public string $dataClass, - public ValidationType $validationType, + public ValidationStrategy $validationStrategy, public bool $mapPropertyNames, public bool $withoutMagicalCreation, public ?array $ignoredMagicalMethods, @@ -46,7 +46,7 @@ public static function createFromConfig( return new self( dataClass: $dataClass, - validationType: ValidationType::from($config['validation_type']), + validationStrategy: ValidationStrategy::from($config['validation_strategy']), mapPropertyNames: true, withoutMagicalCreation: false, ignoredMagicalMethods: null, @@ -59,7 +59,7 @@ public static function createFromContext( ) { return new self( dataClass: $context->dataClass, - validationType: $context->validationType, + validationStrategy: $context->validationStrategy, mapPropertyNames: $context->mapPropertyNames, withoutMagicalCreation: $context->withoutMagicalCreation, ignoredMagicalMethods: $context->ignoredMagicalMethods, @@ -67,30 +67,30 @@ public static function createFromContext( ); } - public function validationType(ValidationType $validationType): self + public function validationStrategy(ValidationStrategy $validationStrategy): self { - $this->validationType = $validationType; + $this->validationStrategy = $validationStrategy; return $this; } - public function disableValidation(): self + public function withoutValidation(): self { - $this->validationType = ValidationType::Disabled; + $this->validationStrategy = ValidationStrategy::Disabled; return $this; } public function onlyValidateRequests(): self { - $this->validationType = ValidationType::OnlyRequests; + $this->validationStrategy = ValidationStrategy::OnlyRequests; return $this; } public function alwaysValidate(): self { - $this->validationType = ValidationType::Always; + $this->validationStrategy = ValidationStrategy::Always; return $this; } @@ -155,7 +155,7 @@ public function get(): CreationContext { return new CreationContext( dataClass: $this->dataClass, - validationType: $this->validationType, + validationStrategy: $this->validationStrategy, mapPropertyNames: $this->mapPropertyNames, withoutMagicalCreation: $this->withoutMagicalCreation, ignoredMagicalMethods: $this->ignoredMagicalMethods, diff --git a/src/Support/Creation/ValidationType.php b/src/Support/Creation/ValidationStrategy.php similarity index 83% rename from src/Support/Creation/ValidationType.php rename to src/Support/Creation/ValidationStrategy.php index fbef2165..a4de166d 100644 --- a/src/Support/Creation/ValidationType.php +++ b/src/Support/Creation/ValidationStrategy.php @@ -2,7 +2,7 @@ namespace Spatie\LaravelData\Support\Creation; -enum ValidationType: string +enum ValidationStrategy: string { case Always = 'always'; case OnlyRequests = 'only_requests'; diff --git a/src/Transformers/Transformer.php b/src/Transformers/Transformer.php index 6ef510f3..0ba443cc 100644 --- a/src/Transformers/Transformer.php +++ b/src/Transformers/Transformer.php @@ -7,9 +7,5 @@ interface Transformer { - public function transform( - DataProperty $property, - mixed $value, - TransformationContext $context - ): mixed; + public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed; } diff --git a/tests/CreationFactoryTest.php b/tests/CreationFactoryTest.php index 2c4836ba..985d7f17 100644 --- a/tests/CreationFactoryTest.php +++ b/tests/CreationFactoryTest.php @@ -92,7 +92,7 @@ public static function fromArray(array $payload) expect(fn () => $dataClass::factory()->from($request)) ->toThrow(ValidationException::class); - expect($dataClass::factory()->disableValidation()->from($request)) + expect($dataClass::factory()->withoutValidation()->from($request)) ->toBeInstanceOf(Data::class) ->string->toEqual('nowp'); }); diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 1a2437ba..d2a1f7ac 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -15,13 +15,6 @@ use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithExplicitValidationRuleAttributeData; -function performRequest(string $string): TestResponse -{ - return postJson('/example-route', [ - 'string' => $string, - ]); -} - beforeEach(function () { handleExceptions([ AuthenticationException::class, diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 1b57fc0c..65077c32 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -41,7 +41,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Optional; -use Spatie\LaravelData\Support\Creation\ValidationType; +use Spatie\LaravelData\Support\Creation\ValidationStrategy; use Spatie\LaravelData\Support\Validation\References\FieldReference; use Spatie\LaravelData\Support\Validation\References\RouteParameterReference; use Spatie\LaravelData\Support\Validation\ValidationContext; @@ -2340,7 +2340,7 @@ public static function rules(ValidationContext $context): array expect($dataClass::validateAndCreate([])->toArray())->toBe([]); }); -it('is possible to define the validation type for each data object globally using config', function () { +it('is possible to define the validation strategy for each data object globally using config', function () { $dataClass = new class () extends Data { #[In('Hello World')] public string $string; @@ -2350,7 +2350,7 @@ public static function rules(ValidationContext $context): array ->toBeInstanceOf(Data::class) ->string->toBe('Nowp'); - config()->set('data.validation_type', ValidationType::Always->value); + config()->set('data.validation_strategy', ValidationStrategy::Always->value); expect(fn () => $dataClass::from(['string' => 'Nowp'])) ->toThrow(ValidationException::class); diff --git a/tests/WrapTest.php b/tests/WrapTest.php index 50c39527..66ab9250 100644 --- a/tests/WrapTest.php +++ b/tests/WrapTest.php @@ -1,12 +1,21 @@ $string, + ]); +} it('can wrap data objects by method call', function () { expect( @@ -231,3 +240,4 @@ public function with(): array ], ]); }); + From 1d5d7678ff9a6d184bbe5ac2e7eaf336e4888ef5 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 23 Jan 2024 13:40:31 +0100 Subject: [PATCH 102/124] Permanent includes --- CHANGELOG.md | 2 + docs/as-a-data-transfer-object/collections.md | 6 +- .../creating-a-data-object.md | 32 +++++- docs/as-a-resource/from-data-to-resource.md | 20 +++- docs/as-a-resource/lazy-properties.md | 97 +++++++++++++++---- docs/as-a-resource/transformers.md | 67 +++++++++++-- src/Resource.php | 16 +++ .../Partials/ForwardsToPartialsDefinition.php | 68 ++++++++++--- .../Types/Storage/AcceptedTypesStorage.php | 2 +- tests/PartialsTest.php | 60 ++++++++++++ 10 files changed, 320 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc270f31..f2465f4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,11 +15,13 @@ All notable changes to `laravel-data` will be documented in this file. - Added contexts to the creation and transformation process - Allow creating a data object or collection using a factory - Speed up the process of creating and transforming data objects +- Add support for BNF syntax - Rewritten docs **Some more "internal" changes** - Restructured tests for the future we have ahead +- The Type system was completely rewritten, allowing for a better performance and more flexibility in the future - Benchmarks added to make data even faster ## 3.11.0 - 2023-12-21 diff --git a/docs/as-a-data-transfer-object/collections.md b/docs/as-a-data-transfer-object/collections.md index df1a492e..e0fe6cf9 100644 --- a/docs/as-a-data-transfer-object/collections.md +++ b/docs/as-a-data-transfer-object/collections.md @@ -232,9 +232,9 @@ SongData::collect(Song::all(), DataCollection::class)->first(); // SongData obje In previous versions of the package it was possible to use the `collection` method to create a collection of data objects: ```php -SongData::collection(Song::all()); // returns a DataCollection of SongData objects -SongData::collection(Song::paginate()); // returns a PaginatedDataCollection of SongData objects -SongData::collection(Song::cursorPaginate()); // returns a CursorPaginatedCollection of SongData objects +SongData::collect(Song::all()); // returns a DataCollection of SongData objects +SongData::collect(Song::paginate()); // returns a PaginatedDataCollection of SongData objects +SongData::collect(Song::cursorPaginate()); // returns a CursorPaginatedCollection of SongData objects ``` This method was removed with version v4 of the package in favor for the more powerful `collect` method. The `collection` method can still be used by using the `WithDeprecatedCollectionMethod` trait: diff --git a/docs/as-a-data-transfer-object/creating-a-data-object.md b/docs/as-a-data-transfer-object/creating-a-data-object.md index 905eb84d..a097ed27 100644 --- a/docs/as-a-data-transfer-object/creating-a-data-object.md +++ b/docs/as-a-data-transfer-object/creating-a-data-object.md @@ -1,5 +1,5 @@ --- -title: Creating a data object +title: Creating a data object weight: 1 --- @@ -52,7 +52,8 @@ Data can also be created from JSON strings: SongData::from('{"title" : "Never Gonna Give You Up","artist" : "Rick Astley"}'); ``` -Although the PHP 8.0 constructor properties look great in data objects, it is perfectly valid to use regular properties without a constructor like so: +Although the PHP 8.0 constructor properties look great in data objects, it is perfectly valid to use regular properties +without a constructor like so: ```php class SongData extends Data @@ -160,7 +161,8 @@ will try to create itself from the following types: - An *Arrayable* by calling `toArray` on it - An *array* -This list can be extended using extra normalizers, find more about it [here](https://spatie.be/docs/laravel-data/v4/advanced-usage/normalizers). +This list can be extended using extra normalizers, find more about +it [here](https://spatie.be/docs/laravel-data/v4/advanced-usage/normalizers). When a data object cannot be created using magical methods or the default methods, a `CannotCreateData` exception will be thrown. @@ -187,4 +189,26 @@ SongData::withoutMagicalCreationFrom($song); ## Advanced creation using factories -It is possible to configure how a data object is created, whether it will be validated, which casts to use and more. You can read more about it [here](/docs/laravel-data/v4/advanced-usage/factories). +It is possible to configure how a data object is created, whether it will be validated, which casts to use and more. You +can read more about it [here](/docs/laravel-data/v4/advanced-usage/factories). + +## DTO classes + +The default `Data` class from which you extend your data objects is a multi versatile class, it packs a lot of +functionality. But sometimes you just want a simple DTO class. You can use the `Dto` class for this: + +```php +class SongData extends Dto +{ + public function __construct( + public string $title, + public string $artist, + ) { + } +} +``` + +The `Dto` class is a data class in its most basic form. It can br created from anything using magical methods, can +validate payloads before creating the data object and can be created using factories. But it doesn't have any of the +other functionality that the `Data` class has. + diff --git a/docs/as-a-resource/from-data-to-resource.md b/docs/as-a-resource/from-data-to-resource.md index efbe389f..29a806b3 100644 --- a/docs/as-a-resource/from-data-to-resource.md +++ b/docs/as-a-resource/from-data-to-resource.md @@ -170,7 +170,25 @@ SongData::empty([ ]); ``` - ## Response status code +## Response status code When a resource is being returned from a controller, the status code of the response will automatically be set to `201 CREATED` when Laravel data detects that the request's method is `POST`. In all other cases, `200 OK` will be returned. +## Resource classes + +To make it a bit more clear that a data object is a resource, you can use the `Resource` class instead of the `Data` class: + +```php +use Spatie\LaravelData\Resource; + +class SongResource extends Resource +{ + public function __construct( + public string $title, + public string $artist, + ) { + } +} +``` + +These resource classes have as an advantage that they won't validate data or check authorization, They are only used to transform data which makes them a bit faster. diff --git a/docs/as-a-resource/lazy-properties.md b/docs/as-a-resource/lazy-properties.md index d812b548..e491f2bb 100644 --- a/docs/as-a-resource/lazy-properties.md +++ b/docs/as-a-resource/lazy-properties.md @@ -8,10 +8,12 @@ Sometimes you don't want all the properties included when transforming a data ob ```php class AlbumData extends Data { + /** + * @param Collection $songs + */ public function __construct( public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, + public Collection $songs, ) { } } @@ -22,10 +24,12 @@ This will always output a collection of songs, which can become quite large. Wit ```php class AlbumData extends Data { + /** + * @param Lazy|Collection $songs + */ public function __construct( public string $title, - #[DataCollectionOf(SongData::class)] - public Lazy|DataCollection $songs, + public Lazy|Collection $songs, ) { } @@ -33,7 +37,7 @@ class AlbumData extends Data { return new self( $album->title, - Lazy::create(fn() => SongData::collection($album->songs)) + Lazy::create(fn() => SongData::collect($album->songs)) ); } } @@ -47,15 +51,15 @@ Now when we transform the data object as such: AlbumData::from(Album::first())->toArray(); ``` -We get the following JSON: +We get the following array: -```json -{ - "name": "Together Forever" -} +```php +[ + 'title' => 'Together Forever', +] ``` -As you can see, the `songs` property is missing in the JSON output. Here's how you can include it. +As you can see, the `songs` property is missing in the array output. Here's how you can include it. ```php AlbumData::from(Album::first())->include('songs'); @@ -63,7 +67,7 @@ AlbumData::from(Album::first())->include('songs'); ## Including lazy properties -Properties will only be included when the `include` method is called on the data object with the property's name. +Lazy properties will only be included when the `include` method is called on the data object with the property's name. It is also possible to nest these includes. For example, let's update the `SongData` class and make all of its properties lazy: @@ -138,7 +142,7 @@ return UserData::from(Auth::user())->include('favorite_song.name'); You can include lazy properties in different ways: ```php -Lazy::create(fn() => SongData::collection($album->songs)); +Lazy::create(fn() => SongData::collect($album->songs)); ``` With a basic `Lazy` property, you must explicitly include it when the data object is transformed. @@ -146,7 +150,7 @@ With a basic `Lazy` property, you must explicitly include it when the data objec Sometimes you only want to include a property when a specific condition is true. This can be done with conditional lazy properties: ```php -Lazy::when(fn() => $this->is_admin, fn() => SongData::collection($album->songs)); +Lazy::when(fn() => $this->is_admin, fn() => SongData::collect($album->songs)); ``` The property will only be included when the `is_admin` property of the data object is true. It is not possible to include the property later on with the `include` method when a condition is not accepted. @@ -156,7 +160,7 @@ The property will only be included when the `is_admin` property of the data obje You can also only include a lazy property when a particular relation is loaded on the model as such: ```php -Lazy::whenLoaded('songs', $album, fn() => SongData::collection($album->songs)); +Lazy::whenLoaded('songs', $album, fn() => SongData::collect($album->songs)); ``` Now the property will only be included when the song's relation is loaded on the model. @@ -166,7 +170,7 @@ Now the property will only be included when the song's relation is loaded on the It is possible to mark a lazy property as included by default: ```php -Lazy::create(fn() => SongData::collection($album->songs))->defaultIncluded(); +Lazy::create(fn() => SongData::collect($album->songs))->defaultIncluded(); ``` The property will now always be included when the data object is transformed. You can explicitly exclude properties that were default included as such: @@ -225,10 +229,12 @@ In some cases you may want to define an include on a class level by implementing ```php class AlbumData extends Data { + /** + * @param Lazy|Collection $songs + */ public function __construct( public string $title, - #[DataCollectionOf(SongData::class)] - public Lazy|DataCollection $songs, + public Lazy|Collection $songs, ) { } @@ -246,10 +252,12 @@ It is even possible to include nested properties: ```php class AlbumData extends Data { + /** + * @param Lazy|Collection $songs + */ public function __construct( public string $title, - #[DataCollectionOf(SongData::class)] - public Lazy|DataCollection $songs, + public Lazy|Collection $songs, ) { } @@ -378,3 +386,52 @@ It is also possible to run exclude, except and only operations on a data object: - You can define **except** in `allowedRequestExcept` and use the `except` key in your query string - You can define **only** in `allowedRequestOnly` and use the `only` key in your query string +## Mutability + +Adding includes/excludes/only/except to a data object will only affect the data object (and its nested chain) once: + +```php +AlbumData::from(Album::first())->include('songs')->toArray(); // will include songs +AlbumData::from(Album::first())->toArray(); // will not include songs +``` + +If you want to add includes/excludes/only/except to a data object and its nested chain that will be used for all future transformations, you can define them in their respective *properties methods: + +```php +class AlbumData extends Data +{ + /** + * @param Lazy|Collection $songs + */ + public function __construct( + public string $title, + public Lazy|Collection $songs, + ) { + } + + public function includeProperties(): array + { + return [ + 'songs' + ]; + } +} +``` + +Or use the permanent methods: + +```php +AlbumData::from(Album::first())->includePermanently('songs'); +AlbumData::from(Album::first())->excludePermanently('songs'); +AlbumData::from(Album::first())->onlyPermanently('songs'); +AlbumData::from(Album::first())->exceptPermanently('songs'); +``` + +When using conditional a includes/excludes/only/except, you can set the permanent flag: + +```php +AlbumData::from(Album::first())->includeWhen('songs', fn(AlbumData $data) => count($data->songs) > 0, permanent: true); +AlbumData::from(Album::first())->excludeWhen('songs', fn(AlbumData $data) => count($data->songs) > 0, permanent: true); +AlbumData::from(Album::first())->onlyWhen('songs', fn(AlbumData $data) => count($data->songs) > 0), permanent: true); +AlbumData::from(Album::first())->except('songs', fn(AlbumData $data) => count($data->songs) > 0, permanent: true); +``` diff --git a/docs/as-a-resource/transformers.md b/docs/as-a-resource/transformers.md index 988184bf..779b4aef 100644 --- a/docs/as-a-resource/transformers.md +++ b/docs/as-a-resource/transformers.md @@ -3,11 +3,11 @@ title: Transforming data weight: 7 --- -Each property of a data object should be transformed into a usable type to communicate via JSON. +Transformers allow you to transform complex types to simple types. This is useful when you want to transform a data object to an array or JSON. No complex transformations are required for the default types (string, bool, int, float, enum and array), but special types like `Carbon` or a Laravel Model will need extra attention. -Transformers are simple classes that will convert a complex type to something simple like a `string` or `int`. For example, we can transform a `Carbon` object to `16-05-1994`, `16-05-1994T00:00:00+00` or something completely different. +Transformers are simple classes that will convert a such complex types to something simple like a `string` or `int`. For example, we can transform a `Carbon` object to `16-05-1994`, `16-05-1994T00:00:00+00` or something completely different. There are two ways you can define transformers: locally and globally. @@ -80,16 +80,65 @@ ArtistData::from($artist)->all(); ## Getting a data object (on steroids) -Internally the package uses the `transform` method for operations like `toArray`, `all`, `toJson` and so on. This method is highly configurable: +Internally the package uses the `transform` method for operations like `toArray`, `all`, `toJson` and so on. This method is highly configurable, when calling it without any arguments it will behave like the `toArray` method: ```php +ArtistData::from($artist)->transform(); +``` + +Producing the following result: + +```php +[ + 'name' => 'Rick Astley', + 'birth_date' => '06-02-1966', +] +``` + +It is possible to disable the transformation of values, which will make the `transform` method behave like the `all` method: + +```php +use Spatie\LaravelData\Support\Transformation\TransformationContext; + ArtistData::from($artist)->transform( - bool $transformValues = true, - WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled, - bool $mapPropertyNames = true, + TransformationContext::create()->withoutTransformingValues() ); ``` -- **$transformValues** when enabled transformers will be used to transform properties, also data objects and collections will be transformed -- **$wrapExecutionType** allows you to set if wrapping is `Enabled` or `Disabled` -- **$mapPropertyNames** uses defined mappers to rename properties when enabled +Outputting the following array: + +```php +[ + 'name' => 'Rick Astley', + 'birth_date' => Carbon::parse('06-02-1966'), +] +``` + +The [mapping of property names](/docs/laravel-data/v4/as-a-resource/mapping-property-names) can also be disabled: + +```php +ArtistData::from($artist)->transform( + TransformationContext::create()->mapPropertyNames(false) +); +``` + +It is possible to enable [wrapping](/docs/laravel-data/v4/as-a-resource/wrapping-data) the data object: + +```php +use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; + +ArtistData::from($artist)->transform( + TransformationContext::create()->wrapExecutionType(WrapExecutionType::Enabled) +); +``` + +Outputting the following array: + +```php +[ + 'data' => [ + 'name' => 'Rick Astley', + 'birth_date' => '06-02-1966', + ], +] +``` diff --git a/src/Resource.php b/src/Resource.php index 5358c12d..2cb81da9 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -17,6 +17,12 @@ use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; +use Spatie\LaravelData\DataPipes\AuthorizedDataPipe; +use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe; +use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe; +use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe; +use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; +use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract { @@ -28,4 +34,14 @@ class Resource implements BaseDataContract, AppendableDataContract, IncludeableD use WrappableData; use EmptyData; use ContextableData; + + public static function pipeline(): DataPipeline + { + return DataPipeline::create() + ->into(static::class) + ->through(MapPropertiesDataPipe::class) + ->through(FillRouteParameterPropertiesDataPipe::class) + ->through(DefaultValuesDataPipe::class) + ->through(CastPropertiesDataPipe::class); + } } diff --git a/src/Support/Partials/ForwardsToPartialsDefinition.php b/src/Support/Partials/ForwardsToPartialsDefinition.php index 54b401fb..988ed7be 100644 --- a/src/Support/Partials/ForwardsToPartialsDefinition.php +++ b/src/Support/Partials/ForwardsToPartialsDefinition.php @@ -27,6 +27,17 @@ public function include(string ...$includes): static return $this; } + public function includePermanently(string ...$includes): static + { + $partialsCollection = $this->getPartialsContainer()->includePartials ??= new PartialsCollection(); + + foreach ($includes as $include) { + $partialsCollection->attach(Partial::create($include, permanent: true)); + } + + return $this; + } + public function exclude(string ...$excludes): static { $partialsCollection = $this->getPartialsContainer()->excludePartials ??= new PartialsCollection(); @@ -38,6 +49,17 @@ public function exclude(string ...$excludes): static return $this; } + public function excludePermanently(string ...$excludes): static + { + $partialsCollection = $this->getPartialsContainer()->excludePartials ??= new PartialsCollection(); + + foreach ($excludes as $exclude) { + $partialsCollection->attach(Partial::create($exclude, permanent: true)); + } + + return $this; + } + public function only(string ...$only): static { $partialsCollection = $this->getPartialsContainer()->onlyPartials ??= new PartialsCollection(); @@ -49,6 +71,17 @@ public function only(string ...$only): static return $this; } + public function onlyPermanently(string ...$only): static + { + $partialsCollection = $this->getPartialsContainer()->onlyPartials ??= new PartialsCollection(); + + foreach ($only as $onlyDefinition) { + $partialsCollection->attach(Partial::create($onlyDefinition, permanent: true)); + } + + return $this; + } + public function except(string ...$except): static { $partialsCollection = $this->getPartialsContainer()->exceptPartials ??= new PartialsCollection(); @@ -60,53 +93,64 @@ public function except(string ...$except): static return $this; } - public function includeWhen(string $include, bool|Closure $condition): static + public function exceptPermanently(string ...$except): static + { + $partialsCollection = $this->getPartialsContainer()->exceptPartials ??= new PartialsCollection(); + + foreach ($except as $exceptDefinition) { + $partialsCollection->attach(Partial::create($exceptDefinition, permanent: true)); + } + + return $this; + } + + public function includeWhen(string $include, bool|Closure $condition, bool $permanent = false): static { $partialsCollection = $this->getPartialsContainer()->includePartials ??= new PartialsCollection(); if (is_callable($condition)) { - $partialsCollection->attach(Partial::createConditional($include, $condition)); + $partialsCollection->attach(Partial::createConditional($include, $condition, permanent: $permanent)); } elseif ($condition === true) { - $partialsCollection->attach(Partial::create($include)); + $partialsCollection->attach(Partial::create($include, permanent: $permanent)); } return $this; } - public function excludeWhen(string $exclude, bool|Closure $condition): static + public function excludeWhen(string $exclude, bool|Closure $condition, bool $permanent = false): static { $partialsCollection = $this->getPartialsContainer()->excludePartials ??= new PartialsCollection(); if (is_callable($condition)) { - $partialsCollection->attach(Partial::createConditional($exclude, $condition)); + $partialsCollection->attach(Partial::createConditional($exclude, $condition, permanent: $permanent)); } elseif ($condition === true) { - $partialsCollection->attach(Partial::create($exclude)); + $partialsCollection->attach(Partial::create($exclude, permanent: $permanent)); } return $this; } - public function onlyWhen(string $only, bool|Closure $condition): static + public function onlyWhen(string $only, bool|Closure $condition, bool $permanent = false): static { $partialsCollection = $this->getPartialsContainer()->onlyPartials ??= new PartialsCollection(); if (is_callable($condition)) { - $partialsCollection->attach(Partial::createConditional($only, $condition)); + $partialsCollection->attach(Partial::createConditional($only, $condition, permanent: $permanent)); } elseif ($condition === true) { - $partialsCollection->attach(Partial::create($only)); + $partialsCollection->attach(Partial::create($only, permanent: $permanent)); } return $this; } - public function exceptWhen(string $except, bool|Closure $condition): static + public function exceptWhen(string $except, bool|Closure $condition, bool $permanent = false): static { $partialsCollection = $this->getPartialsContainer()->exceptPartials ??= new PartialsCollection(); if (is_callable($condition)) { - $partialsCollection->attach(Partial::createConditional($except, $condition)); + $partialsCollection->attach(Partial::createConditional($except, $condition, permanent: $permanent)); } elseif ($condition === true) { - $partialsCollection->attach(Partial::create($except)); + $partialsCollection->attach(Partial::create($except, permanent: $permanent)); } return $this; diff --git a/src/Support/Types/Storage/AcceptedTypesStorage.php b/src/Support/Types/Storage/AcceptedTypesStorage.php index 31fb773f..91ca9d5b 100644 --- a/src/Support/Types/Storage/AcceptedTypesStorage.php +++ b/src/Support/Types/Storage/AcceptedTypesStorage.php @@ -41,7 +41,7 @@ public static function getAcceptedTypes(string $name): array /** @return string[] */ protected static function resolveAcceptedTypes(string $name): array { - if (! class_exists($name)) { + if (! class_exists($name) && ! interface_exists($name)) { return []; } diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 1a432d17..0212e973 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -978,6 +978,66 @@ protected function includeProperties(): array ]); }); +it('can define permanent partials using function call', function ( + Data $data, + Closure $temporaryPartial, + Closure $permanentPartial, + array $expectedFullPayload, + array $expectedPartialPayload +) { + $data = $temporaryPartial($data); + + expect($data->toArray())->toBe($expectedPartialPayload); + expect($data->toArray())->toBe($expectedFullPayload); + + $data = $permanentPartial($data); + + expect($data->toArray())->toBe($expectedPartialPayload); + expect($data->toArray())->toBe($expectedPartialPayload); +})->with(function (){ + yield [ + 'data' => new LazyData( + Lazy::create(fn () => 'Rick Astley'), + ), + 'temporaryPartial' => fn(LazyData $data) => $data->include('name'), + 'permanentPartial' => fn(LazyData $data) => $data->includePermanently('name'), + 'expectedFullPayload' => [], + 'expectedPartialPayload' => ['name' => 'Rick Astley'], + ]; + + yield [ + 'data' => new LazyData( + Lazy::create(fn () => 'Rick Astley')->defaultIncluded(), + ), + 'temporaryPartial' => fn(LazyData $data) => $data->exclude('name'), + 'permanentPartial' => fn(LazyData $data) => $data->excludePermanently('name'), + 'expectedFullPayload' => ['name' => 'Rick Astley'], + 'expectedPartialPayload' => [], + ]; + + yield [ + 'data' => new MultiData( + 'Rick Astley', + 'Never gonna give you up', + ), + 'temporaryPartial' => fn(MultiData $data) => $data->only('first'), + 'permanentPartial' => fn(MultiData $data) => $data->onlyPermanently('first'), + 'expectedFullPayload' => ['first' => 'Rick Astley', 'second' => 'Never gonna give you up'], + 'expectedPartialPayload' => ['first' => 'Rick Astley'], + ]; + + yield [ + 'data' => new MultiData( + 'Rick Astley', + 'Never gonna give you up', + ), + 'temporaryPartial' => fn(MultiData $data) => $data->except('first'), + 'permanentPartial' => fn(MultiData $data) => $data->exceptPermanently('first'), + 'expectedFullPayload' => ['first' => 'Rick Astley', 'second' => 'Never gonna give you up'], + 'expectedPartialPayload' => ['second' => 'Never gonna give you up'], + ]; +}); + it('can set partials on a nested data object and these will be respected', function () { class TestMultiLazyNestedDataWithObjectAndCollection extends Data { From 2761d738db3ab7381b079f601fee0fd733320d0d Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 23 Jan 2024 12:40:55 +0000 Subject: [PATCH 103/124] Fix styling --- src/Resource.php | 2 -- tests/PartialsTest.php | 18 +++++++++--------- tests/RequestTest.php | 1 - tests/WrapTest.php | 5 +++-- 4 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Resource.php b/src/Resource.php index 2cb81da9..9a003d05 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -17,12 +17,10 @@ use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -use Spatie\LaravelData\DataPipes\AuthorizedDataPipe; use Spatie\LaravelData\DataPipes\CastPropertiesDataPipe; use Spatie\LaravelData\DataPipes\DefaultValuesDataPipe; use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe; use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract { diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 0212e973..d5a15a69 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -994,13 +994,13 @@ protected function includeProperties(): array expect($data->toArray())->toBe($expectedPartialPayload); expect($data->toArray())->toBe($expectedPartialPayload); -})->with(function (){ +})->with(function () { yield [ 'data' => new LazyData( Lazy::create(fn () => 'Rick Astley'), ), - 'temporaryPartial' => fn(LazyData $data) => $data->include('name'), - 'permanentPartial' => fn(LazyData $data) => $data->includePermanently('name'), + 'temporaryPartial' => fn (LazyData $data) => $data->include('name'), + 'permanentPartial' => fn (LazyData $data) => $data->includePermanently('name'), 'expectedFullPayload' => [], 'expectedPartialPayload' => ['name' => 'Rick Astley'], ]; @@ -1009,8 +1009,8 @@ protected function includeProperties(): array 'data' => new LazyData( Lazy::create(fn () => 'Rick Astley')->defaultIncluded(), ), - 'temporaryPartial' => fn(LazyData $data) => $data->exclude('name'), - 'permanentPartial' => fn(LazyData $data) => $data->excludePermanently('name'), + 'temporaryPartial' => fn (LazyData $data) => $data->exclude('name'), + 'permanentPartial' => fn (LazyData $data) => $data->excludePermanently('name'), 'expectedFullPayload' => ['name' => 'Rick Astley'], 'expectedPartialPayload' => [], ]; @@ -1020,8 +1020,8 @@ protected function includeProperties(): array 'Rick Astley', 'Never gonna give you up', ), - 'temporaryPartial' => fn(MultiData $data) => $data->only('first'), - 'permanentPartial' => fn(MultiData $data) => $data->onlyPermanently('first'), + 'temporaryPartial' => fn (MultiData $data) => $data->only('first'), + 'permanentPartial' => fn (MultiData $data) => $data->onlyPermanently('first'), 'expectedFullPayload' => ['first' => 'Rick Astley', 'second' => 'Never gonna give you up'], 'expectedPartialPayload' => ['first' => 'Rick Astley'], ]; @@ -1031,8 +1031,8 @@ protected function includeProperties(): array 'Rick Astley', 'Never gonna give you up', ), - 'temporaryPartial' => fn(MultiData $data) => $data->except('first'), - 'permanentPartial' => fn(MultiData $data) => $data->exceptPermanently('first'), + 'temporaryPartial' => fn (MultiData $data) => $data->except('first'), + 'permanentPartial' => fn (MultiData $data) => $data->exceptPermanently('first'), 'expectedFullPayload' => ['first' => 'Rick Astley', 'second' => 'Never gonna give you up'], 'expectedPartialPayload' => ['second' => 'Never gonna give you up'], ]; diff --git a/tests/RequestTest.php b/tests/RequestTest.php index d2a1f7ac..a1fa48ac 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -4,7 +4,6 @@ use Illuminate\Auth\AuthenticationException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -use Illuminate\Testing\TestResponse; use Illuminate\Validation\ValidationException; use function Pest\Laravel\handleExceptions; diff --git a/tests/WrapTest.php b/tests/WrapTest.php index 66ab9250..57c6c7df 100644 --- a/tests/WrapTest.php +++ b/tests/WrapTest.php @@ -2,13 +2,15 @@ use Illuminate\Support\Facades\Route; use Illuminate\Testing\TestResponse; + +use function Pest\Laravel\postJson; + use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Tests\Fakes\MultiNestedData; use Spatie\LaravelData\Tests\Fakes\NestedData; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDataWithWrap; -use function Pest\Laravel\postJson; function performRequest(string $string): TestResponse { @@ -240,4 +242,3 @@ public function with(): array ], ]); }); - From fcc0d8f456b728a1f445d7ce3948e7bf61c96d9c Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 23 Jan 2024 14:40:59 +0100 Subject: [PATCH 104/124] Fix differences between contexts --- docs/advanced-usage/creating-a-cast.md | 2 +- .../creating-a-rule-inferrer.md | 2 +- docs/advanced-usage/creating-a-transformer.md | 16 ++- docs/advanced-usage/normalizers.md | 4 + docs/advanced-usage/pipeline.md | 13 +- docs/advanced-usage/typescript.md | 16 --- docs/advanced-usage/use-with-inertia.md | 2 +- docs/advanced-usage/use-with-livewire.md | 2 +- docs/advanced-usage/validation-attributes.md | 2 +- docs/advanced-usage/working-with-dates.md | 2 +- docs/as-a-resource/transformers.md | 6 +- src/Concerns/BaseDataCollectable.php | 2 +- src/Concerns/ResponsableData.php | 2 +- src/Concerns/TransformableData.php | 2 +- src/DataPipes/DataPipe.php | 10 +- src/Support/Creation/CreationContext.php | 1 - .../Creation/CreationContextFactory.php | 36 ++--- .../GlobalCastsCollection.php | 2 +- src/Support/DataConfig.php | 2 +- .../GlobalTransformersCollection.php | 2 +- .../TransformationContextFactory.php | 60 +++----- tests/CreationFactoryTest.php | 2 +- tests/MappingTest.php | 2 +- .../Creation/CreationContextFactoryTest.php | 130 ++++++++++++++++++ .../TransformationContextFactoryTest.php | 85 ++++++++++++ tests/TransformationTest.php | 2 +- 26 files changed, 298 insertions(+), 109 deletions(-) rename src/Support/{Casting => Creation}/GlobalCastsCollection.php (96%) create mode 100644 tests/Support/Creation/CreationContextFactoryTest.php create mode 100644 tests/Support/Transformation/TransformationContextFactoryTest.php diff --git a/docs/advanced-usage/creating-a-cast.md b/docs/advanced-usage/creating-a-cast.md index fe0cc75a..be6e0912 100644 --- a/docs/advanced-usage/creating-a-cast.md +++ b/docs/advanced-usage/creating-a-cast.md @@ -1,6 +1,6 @@ --- title: Creating a cast -weight: 8 +weight: 6 --- Casts take simple values and cast them into complex types. For example, `16-05-1994T00:00:00+00` could be cast into a `Carbon` object with the same date. diff --git a/docs/advanced-usage/creating-a-rule-inferrer.md b/docs/advanced-usage/creating-a-rule-inferrer.md index a37bb106..10a39b5f 100644 --- a/docs/advanced-usage/creating-a-rule-inferrer.md +++ b/docs/advanced-usage/creating-a-rule-inferrer.md @@ -1,6 +1,6 @@ --- title: Creating a rule inferrer -weight: 10 +weight: 8 --- Rule inferrers will try to infer validation rules for properties within a data object. diff --git a/docs/advanced-usage/creating-a-transformer.md b/docs/advanced-usage/creating-a-transformer.md index 95660b77..397ec835 100644 --- a/docs/advanced-usage/creating-a-transformer.md +++ b/docs/advanced-usage/creating-a-transformer.md @@ -1,6 +1,6 @@ --- title: Creating a transformer -weight: 9 +weight: 7 --- Transformers take complex values and transform them into simple types. For example, a `Carbon` object could be transformed to `16-05-1994T00:00:00+00`. @@ -10,10 +10,18 @@ A transformer implements the following interface: ```php interface Transformer { - public function transform(DataProperty $property, mixed $value): mixed; + public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed; } ``` -The value that should be transformed is given, and a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures). +The following parameters are provided: -In the end, the transformer should return a transformed value. Please note that the given value of a transformer can never be `null`. +- **property**: a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures) +- **value**: the value that should be transformed, this will never be `null` +- **context**: a `TransformationContext` object which contains the current transformation context with the following properties: + - **transformValues** indicates if values should be transformed or not + - **mapPropertyNames** indicates if property names should be mapped or not + - **wrapExecutionType** the execution type that should be used for wrapping values + - **transformers** a collection of transformers that can be used to transform values + +In the end, the transformer should return a transformed value. diff --git a/docs/advanced-usage/normalizers.md b/docs/advanced-usage/normalizers.md index 51507f20..609c613b 100644 --- a/docs/advanced-usage/normalizers.md +++ b/docs/advanced-usage/normalizers.md @@ -20,6 +20,10 @@ By default, there are five normalizers for each data object: - **ArrayNormalizer** will cast arrays - **JsonNormalizer** will cast json strings +A sixth normalizer can be optionally enabled: + +- **FormRequestNormalizer** will normalize a form request by calling the `validated` method + Normalizers can be globally configured in `config/data.php`, and can be configured on a specific data object by overriding the `normalizers` method. ```php diff --git a/docs/advanced-usage/pipeline.md b/docs/advanced-usage/pipeline.md index 333e11b1..f3f1aafb 100644 --- a/docs/advanced-usage/pipeline.md +++ b/docs/advanced-usage/pipeline.md @@ -14,6 +14,7 @@ By default, the pipeline exists of the following pipes: - **AuthorizedDataPipe** checks if the user is authorized to perform the request - **MapPropertiesDataPipe** maps the names of properties +- **FillRouteParameterPropertiesDataPipe** fills property values from route parameters - **ValidatePropertiesDataPipe** validates the properties - **DefaultValuesDataPipe** adds default values for properties when they are not set - **CastPropertiesDataPipe** casts the values of properties @@ -35,6 +36,7 @@ class SongData extends Data ->into(static::class) ->through(AuthorizedDataPipe::class) ->through(MapPropertiesDataPipe::class) + ->through(FillRouteParameterPropertiesDataPipe::class) ->through(ValidatePropertiesDataPipe::class) ->through(DefaultValuesDataPipe::class) ->through(CastPropertiesDataPipe::class); @@ -42,12 +44,12 @@ class SongData extends Data } ``` -Each pipe implements the `DataPipe` interface and should return a `Collection` of properties: +Each pipe implements the `DataPipe` interface and should return an `array` of properties: ```php interface DataPipe { - public function handle(mixed $payload, DataClass $class, Collection $properties): Collection; + public function handle(mixed $payload, DataClass $class, array $properties, CreationContext $creationContext): array; } ``` @@ -57,6 +59,13 @@ The `handle` method has several arguments: - **class** the `DataClass` object for the data object [more info](/docs/laravel-data/v4/advanced-usage/internal-structures) - **properties** the key-value properties which will be used to construct the data object +- **creationContext** the context in which the data object is being created you'll find the following info here: + - **dataClass** the data class which is being created + - **validationStrategy** the validation strategy which is being used + - **mapPropertyNames** whether property names should be mapped + - **withoutMagicalCreation** whether to use the magical creation methods or not + - **ignoredMagicalMethods** the magical methods which are ignored + - **casts** a collection of global casts When using a magic creation methods, the pipeline is not being used (since you manually overwrite how a data object is constructed). Only when you pass in a request object a minimal version of the pipeline is used to authorize and validate diff --git a/docs/advanced-usage/typescript.md b/docs/advanced-usage/typescript.md index 66e5d29a..08de7603 100644 --- a/docs/advanced-usage/typescript.md +++ b/docs/advanced-usage/typescript.md @@ -128,19 +128,3 @@ class DataObject extends Data } } ``` - -You can also make all the properties of a data object optional in TypeScript like this: - -```php -class DataObject extends Data -{ - #[TypeScriptOptional] - public function __construct( - public int $id, - public string $someString, - public Optional|string $optional, - ) - { - } -} -``` diff --git a/docs/advanced-usage/use-with-inertia.md b/docs/advanced-usage/use-with-inertia.md index ada92a26..4f7162e0 100644 --- a/docs/advanced-usage/use-with-inertia.md +++ b/docs/advanced-usage/use-with-inertia.md @@ -1,6 +1,6 @@ --- title: Use with Inertia -weight: 6 +weight: 9 --- > Inertia.js lets you quickly build modern single-page React, Vue, and Svelte apps using classic server-side routing and controllers. diff --git a/docs/advanced-usage/use-with-livewire.md b/docs/advanced-usage/use-with-livewire.md index ac832b89..57d528a3 100644 --- a/docs/advanced-usage/use-with-livewire.md +++ b/docs/advanced-usage/use-with-livewire.md @@ -1,6 +1,6 @@ --- title: Use with Livewire -weight: 7 +weight: 10 --- > Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple without leaving the comfort of Laravel. diff --git a/docs/advanced-usage/validation-attributes.md b/docs/advanced-usage/validation-attributes.md index 58eb28ca..25d0a6cf 100644 --- a/docs/advanced-usage/validation-attributes.md +++ b/docs/advanced-usage/validation-attributes.md @@ -1,6 +1,6 @@ --- title: Validation attributes -weight: 16 +weight: 14 --- These are all the validation attributes currently available in laravel-data. diff --git a/docs/advanced-usage/working-with-dates.md b/docs/advanced-usage/working-with-dates.md index 0ae42544..1cd57602 100644 --- a/docs/advanced-usage/working-with-dates.md +++ b/docs/advanced-usage/working-with-dates.md @@ -54,7 +54,7 @@ within the `data.php` config file: Now when casting a date, a valid format will be searched. When none can be found, an exception is thrown. -When a transformers hasn't explicitly stated it's format, the first format within the array is used. +When a transformers hasn't explicitly stated its format, the first format within the array is used. ## Casting dates in a different time zone diff --git a/docs/as-a-resource/transformers.md b/docs/as-a-resource/transformers.md index 779b4aef..1d678e60 100644 --- a/docs/as-a-resource/transformers.md +++ b/docs/as-a-resource/transformers.md @@ -101,7 +101,7 @@ It is possible to disable the transformation of values, which will make the `tra use Spatie\LaravelData\Support\Transformation\TransformationContext; ArtistData::from($artist)->transform( - TransformationContext::create()->withoutTransformingValues() + TransformationContextFactory::create()->transformValues(false) ); ``` @@ -118,7 +118,7 @@ The [mapping of property names](/docs/laravel-data/v4/as-a-resource/mapping-prop ```php ArtistData::from($artist)->transform( - TransformationContext::create()->mapPropertyNames(false) + TransformationContextFactory::create()->mapPropertyNames(false) ); ``` @@ -128,7 +128,7 @@ It is possible to enable [wrapping](/docs/laravel-data/v4/as-a-resource/wrapping use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; ArtistData::from($artist)->transform( - TransformationContext::create()->wrapExecutionType(WrapExecutionType::Enabled) + TransformationContextFactory::create()->wrapExecutionType(WrapExecutionType::Enabled) ); ``` diff --git a/src/Concerns/BaseDataCollectable.php b/src/Concerns/BaseDataCollectable.php index 4b90d5db..e1041b6f 100644 --- a/src/Concerns/BaseDataCollectable.php +++ b/src/Concerns/BaseDataCollectable.php @@ -24,7 +24,7 @@ public function getDataClass(): string public function getIterator(): ArrayIterator { /** @var array $data */ - $data = $this->transform(TransformationContextFactory::create()->transformValues(false)); + $data = $this->transform(TransformationContextFactory::create()->withValueTransformation(false)); return new ArrayIterator($data); } diff --git a/src/Concerns/ResponsableData.php b/src/Concerns/ResponsableData.php index 1371809f..33578544 100644 --- a/src/Concerns/ResponsableData.php +++ b/src/Concerns/ResponsableData.php @@ -15,7 +15,7 @@ trait ResponsableData public function toResponse($request) { $contextFactory = TransformationContextFactory::create() - ->wrapExecutionType(WrapExecutionType::Enabled); + ->withWrapExecutionType(WrapExecutionType::Enabled); $includePartials = DataContainer::get()->requestQueryStringPartialsResolver()->execute( $this, diff --git a/src/Concerns/TransformableData.php b/src/Concerns/TransformableData.php index 09ffe7c6..255d488b 100644 --- a/src/Concerns/TransformableData.php +++ b/src/Concerns/TransformableData.php @@ -37,7 +37,7 @@ public function transform( public function all(): array { - return $this->transform(TransformationContextFactory::create()->transformValues(false)); + return $this->transform(TransformationContextFactory::create()->withValueTransformation(false)); } public function toArray(): array diff --git a/src/DataPipes/DataPipe.php b/src/DataPipes/DataPipe.php index ceb7f64f..5e411ce5 100644 --- a/src/DataPipes/DataPipe.php +++ b/src/DataPipes/DataPipe.php @@ -8,17 +8,9 @@ interface DataPipe { /** - * @param mixed $payload - * @param DataClass $class * @param array $properties - * @param CreationContext $creationContext * * @return array */ - public function handle( - mixed $payload, - DataClass $class, - array $properties, - CreationContext $creationContext - ): array; + public function handle(mixed $payload, DataClass $class, array $properties, CreationContext $creationContext): array; } diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php index 030b6709..570b4592 100644 --- a/src/Support/Creation/CreationContext.php +++ b/src/Support/Creation/CreationContext.php @@ -16,7 +16,6 @@ use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Casting\GlobalCastsCollection; use Spatie\LaravelData\Support\DataContainer; /** diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index f630a409..4dad0b1e 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -17,7 +17,6 @@ use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Casting\GlobalCastsCollection; use Spatie\LaravelData\Support\DataContainer; /** @@ -32,7 +31,7 @@ public function __construct( public string $dataClass, public ValidationStrategy $validationStrategy, public bool $mapPropertyNames, - public bool $withoutMagicalCreation, + public bool $disableMagicalCreation, public ?array $ignoredMagicalMethods, public ?GlobalCastsCollection $casts, ) { @@ -48,25 +47,12 @@ public static function createFromConfig( dataClass: $dataClass, validationStrategy: ValidationStrategy::from($config['validation_strategy']), mapPropertyNames: true, - withoutMagicalCreation: false, + disableMagicalCreation: false, ignoredMagicalMethods: null, casts: null, ); } - public static function createFromContext( - CreationContext $context - ) { - return new self( - dataClass: $context->dataClass, - validationStrategy: $context->validationStrategy, - mapPropertyNames: $context->mapPropertyNames, - withoutMagicalCreation: $context->withoutMagicalCreation, - ignoredMagicalMethods: $context->ignoredMagicalMethods, - casts: $context->casts, - ); - } - public function validationStrategy(ValidationStrategy $validationStrategy): self { $this->validationStrategy = $validationStrategy; @@ -95,6 +81,13 @@ public function alwaysValidate(): self return $this; } + public function withPropertyNameMapping(bool $withPropertyNameMapping = true): self + { + $this->mapPropertyNames = $withPropertyNameMapping; + + return $this; + } + public function withoutPropertyNameMapping(bool $withoutPropertyNameMapping = true): self { $this->mapPropertyNames = ! $withoutPropertyNameMapping; @@ -104,7 +97,14 @@ public function withoutPropertyNameMapping(bool $withoutPropertyNameMapping = tr public function withoutMagicalCreation(bool $withoutMagicalCreation = true): self { - $this->withoutMagicalCreation = $withoutMagicalCreation; + $this->disableMagicalCreation = $withoutMagicalCreation; + + return $this; + } + + public function withMagicalCreation(bool $withMagicalCreation = true): self + { + $this->disableMagicalCreation = ! $withMagicalCreation; return $this; } @@ -157,7 +157,7 @@ public function get(): CreationContext dataClass: $this->dataClass, validationStrategy: $this->validationStrategy, mapPropertyNames: $this->mapPropertyNames, - withoutMagicalCreation: $this->withoutMagicalCreation, + withoutMagicalCreation: $this->disableMagicalCreation, ignoredMagicalMethods: $this->ignoredMagicalMethods, casts: $this->casts, ); diff --git a/src/Support/Casting/GlobalCastsCollection.php b/src/Support/Creation/GlobalCastsCollection.php similarity index 96% rename from src/Support/Casting/GlobalCastsCollection.php rename to src/Support/Creation/GlobalCastsCollection.php index 50eabb70..9a4a311f 100644 --- a/src/Support/Casting/GlobalCastsCollection.php +++ b/src/Support/Creation/GlobalCastsCollection.php @@ -1,6 +1,6 @@ transformers[get_debug_type($value)] ?? null; } foreach ($this->transformers as $transformable => $transformer) { diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 61bddd0d..45798c4e 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -94,87 +94,65 @@ public function get( ); } - public function transformValues(bool $transformValues = true): static + public function withValueTransformation(bool $transformValues = true): static { $this->transformValues = $transformValues; return $this; } - public function mapPropertyNames(bool $mapPropertyNames = true): static + public function withoutValueTransformation(bool $withoutValueTransformation = true): static { - $this->mapPropertyNames = $mapPropertyNames; + $this->transformValues = ! $withoutValueTransformation; return $this; } - public function wrapExecutionType(WrapExecutionType $wrapExecutionType): static + public function withPropertyNameMapping(bool $mapPropertyNames = true): static { - $this->wrapExecutionType = $wrapExecutionType; + $this->mapPropertyNames = $mapPropertyNames; return $this; } - public function transformer(string $transformable, Transformer $transformer): static + public function withoutPropertyNameMapping(bool $withoutPropertyNameMapping = true): static { - if ($this->transformers === null) { - $this->transformers = new GlobalTransformersCollection(); - } - - $this->transformers->add($transformable, $transformer); + $this->mapPropertyNames = ! $withoutPropertyNameMapping; return $this; } - public function addIncludePartial(Partial ...$partial): static + public function withWrapExecutionType(WrapExecutionType $wrapExecutionType): static { - if ($this->includePartials === null) { - $this->includePartials = new PartialsCollection(); - } - - foreach ($partial as $include) { - $this->includePartials->attach($include); - } + $this->wrapExecutionType = $wrapExecutionType; return $this; } - public function addExcludePartial(Partial ...$partial): static + public function withoutWrapping(): static { - if ($this->excludePartials === null) { - $this->excludePartials = new PartialsCollection(); - } - - foreach ($partial as $exclude) { - $this->excludePartials->attach($exclude); - } + $this->wrapExecutionType = WrapExecutionType::Disabled; return $this; } - public function addOnlyPartial(Partial ...$partial): static + public function withWrapping(): static { - if ($this->onlyPartials === null) { - $this->onlyPartials = new PartialsCollection(); - } - - foreach ($partial as $only) { - $this->onlyPartials->attach($only); - } + $this->wrapExecutionType = WrapExecutionType::Enabled; return $this; } - public function addExceptPartial(Partial ...$partial): static + public function withTransformer(string $transformable, Transformer|string $transformer): static { - if ($this->exceptPartials === null) { - $this->exceptPartials = new PartialsCollection(); - } + $transformer = is_string($transformer) ? app($transformer) : $transformer; - foreach ($partial as $except) { - $this->exceptPartials->attach($except); + if ($this->transformers === null) { + $this->transformers = new GlobalTransformersCollection(); } + $this->transformers->add($transformable, $transformer); + return $this; } diff --git a/tests/CreationFactoryTest.php b/tests/CreationFactoryTest.php index 985d7f17..c512024c 100644 --- a/tests/CreationFactoryTest.php +++ b/tests/CreationFactoryTest.php @@ -5,7 +5,7 @@ use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\Validation\In; use Spatie\LaravelData\Data; -use Spatie\LaravelData\Support\Casting\GlobalCastsCollection; +use Spatie\LaravelData\Support\Creation\GlobalCastsCollection; use Spatie\LaravelData\Tests\Fakes\Casts\MeaningOfLifeCast; use Spatie\LaravelData\Tests\Fakes\Casts\StringToUpperCast; use Spatie\LaravelData\Tests\Fakes\SimpleData; diff --git a/tests/MappingTest.php b/tests/MappingTest.php index 74412fc7..ebc2c5a8 100644 --- a/tests/MappingTest.php +++ b/tests/MappingTest.php @@ -94,7 +94,7 @@ public function __construct( } }; - expect($data)->transform(TransformationContextFactory::create()->mapPropertyNames(false)) + expect($data)->transform(TransformationContextFactory::create()->withPropertyNameMapping(false)) ->toMatchArray([ 'camelName' => 'Freek', ]); diff --git a/tests/Support/Creation/CreationContextFactoryTest.php b/tests/Support/Creation/CreationContextFactoryTest.php new file mode 100644 index 00000000..b6b68f25 --- /dev/null +++ b/tests/Support/Creation/CreationContextFactoryTest.php @@ -0,0 +1,130 @@ +dataClass)->toBe(SimpleData::class); + expect($context->validationStrategy)->toBe(ValidationStrategy::from(config('data.validation_strategy'))); + expect($context->mapPropertyNames)->toBeTrue(); + expect($context->disableMagicalCreation)->toBeFalse(); + expect($context->ignoredMagicalMethods)->toBeNull(); + expect($context->casts)->toBeNull(); +}); + +it('is possible to override the validation strategy', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->validationStrategy(ValidationStrategy::OnlyRequests); + + expect($context->validationStrategy)->toBe(ValidationStrategy::OnlyRequests); +}); + +it('is possible to disable validation', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->withoutValidation(); + + expect($context->validationStrategy)->toBe(ValidationStrategy::Disabled); +}); + +it('is possible to only validate requests', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->onlyValidateRequests(); + + expect($context->validationStrategy)->toBe(ValidationStrategy::OnlyRequests); +}); + +it('is possible to always validate', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->alwaysValidate(); + + expect($context->validationStrategy)->toBe(ValidationStrategy::Always); +}); + +it('is possible to disable property name mapping', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->withoutPropertyNameMapping(); + + expect($context->mapPropertyNames)->toBeFalse(); +}); + +it('is possible to enable property name mapping', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->withPropertyNameMapping(); + + expect($context->mapPropertyNames)->toBeTrue(); +}); + +it('is possible to disable magical creation', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->withoutMagicalCreation(); + + expect($context->disableMagicalCreation)->toBeTrue(); +}); + +it('is possible to enable magical creation', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->withMagicalCreation(); + + expect($context->disableMagicalCreation)->toBeFalse(); +}); + +it('is possible to set ignored magical methods', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->ignoreMagicalMethod('foo', 'bar'); + + expect($context->ignoredMagicalMethods)->toBe(['foo', 'bar']); +}); + +it('is possible to add a cast', function () { + $context = CreationContextFactory::createFromConfig( + SimpleData::class + )->withCast('string', StringToUpperCast::class); + + $dataClass = new class extends Data + { + public string $string; + }; + + $dataProperty = app(DataConfig::class)->getDataClass($dataClass::class)->properties['string']; + + expect($context->casts) + ->not()->toBeNull() + ->findCastForValue($dataProperty)->toBeInstanceOf(StringToUpperCast::class); +}); + +it('is possible to add a cast collection', function (){ + $context = CreationContextFactory::createFromConfig(SimpleData::class) + ->withCast(\Illuminate\Support\Stringable::class, StringToUpperCast::class) + ->withCastCollection(new GlobalCastsCollection([ + 'string' => new StringToUpperCast() + ])); + + $dataClass = new class extends Data + { + public string $string; + }; + + $dataProperty = app(DataConfig::class)->getDataClass($dataClass::class)->properties['string']; + + expect($context->casts) + ->not()->toBeNull() + ->findCastForValue($dataProperty)->toBeInstanceOf(StringToUpperCast::class); +}); diff --git a/tests/Support/Transformation/TransformationContextFactoryTest.php b/tests/Support/Transformation/TransformationContextFactoryTest.php new file mode 100644 index 00000000..dcb3e3a9 --- /dev/null +++ b/tests/Support/Transformation/TransformationContextFactoryTest.php @@ -0,0 +1,85 @@ +get( + SimpleData::from('Hello World') + ); + + expect($context)->toBeInstanceOf(TransformationContext::class); + expect($context->transformValues)->toBeTrue(); + expect($context->mapPropertyNames)->toBeTrue(); + expect($context->wrapExecutionType)->toBe(WrapExecutionType::Disabled); + expect($context->transformers)->toBeNull(); +}); + +it('can disable value transformation', function () { + $context = TransformationContextFactory::create() + ->withoutValueTransformation() + ->get(SimpleData::from('Hello World')); + + expect($context->transformValues)->toBeFalse(); +}); + +it('can enable value transformation', function () { + $context = TransformationContextFactory::create() + ->withValueTransformation() + ->get(SimpleData::from('Hello World')); + + expect($context->transformValues)->toBeTrue(); +}); + +it('can disable property name mapping', function () { + $context = TransformationContextFactory::create() + ->withoutPropertyNameMapping() + ->get(SimpleData::from('Hello World')); + + expect($context->mapPropertyNames)->toBeFalse(); +}); + +it('can enable property name mapping', function () { + $context = TransformationContextFactory::create() + ->withPropertyNameMapping() + ->get(SimpleData::from('Hello World')); + + expect($context->mapPropertyNames)->toBeTrue(); +}); + +it('can disable wrapping', function () { + $context = TransformationContextFactory::create() + ->withoutWrapping() + ->get(SimpleData::from('Hello World')); + + expect($context->wrapExecutionType)->toBe(WrapExecutionType::Disabled); +}); + +it('can enable wrapping', function () { + $context = TransformationContextFactory::create() + ->withWrapping() + ->get(SimpleData::from('Hello World')); + + expect($context->wrapExecutionType)->toBe(WrapExecutionType::Enabled); +}); + +it('can set a custom wrap execution type', function () { + $context = TransformationContextFactory::create() + ->withWrapExecutionType(WrapExecutionType::Enabled) + ->get(SimpleData::from('Hello World')); + + expect($context->wrapExecutionType)->toBe(WrapExecutionType::Enabled); +}); + +it('can add a custom transformers', function () { + $context = TransformationContextFactory::create() + ->withTransformer('string', StringToUpperTransformer::class) + ->get(SimpleData::from('Hello World')); + + expect($context->transformers)->not()->toBe(null); + expect($context->transformers->findTransformerForValue('Hello World'))->toBeInstanceOf(StringToUpperTransformer::class); +}); diff --git a/tests/TransformationTest.php b/tests/TransformationTest.php index 0dd8c7e0..f2409a19 100644 --- a/tests/TransformationTest.php +++ b/tests/TransformationTest.php @@ -311,7 +311,7 @@ public function transform(DataProperty $property, mixed $value, TransformationCo }; $transformed = $data->transform( - TransformationContextFactory::create()->transformer(DateTimeInterface::class, $customTransformer) + TransformationContextFactory::create()->withTransformer(DateTimeInterface::class, $customTransformer) ); expect($transformed)->toBe([ From a529ea4d1677ade0f4378415b6f5af6eb1697a08 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 23 Jan 2024 13:41:36 +0000 Subject: [PATCH 105/124] Fix styling --- .../Transformation/TransformationContextFactory.php | 1 - tests/Support/Creation/CreationContextFactoryTest.php | 10 ++++------ .../TransformationContextFactoryTest.php | 1 - 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Support/Transformation/TransformationContextFactory.php b/src/Support/Transformation/TransformationContextFactory.php index 45798c4e..879d5028 100644 --- a/src/Support/Transformation/TransformationContextFactory.php +++ b/src/Support/Transformation/TransformationContextFactory.php @@ -5,7 +5,6 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Support\Partials\ForwardsToPartialsDefinition; -use Spatie\LaravelData\Support\Partials\Partial; use Spatie\LaravelData\Support\Partials\PartialsCollection; use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; use Spatie\LaravelData\Transformers\Transformer; diff --git a/tests/Support/Creation/CreationContextFactoryTest.php b/tests/Support/Creation/CreationContextFactoryTest.php index b6b68f25..b5e86e5b 100644 --- a/tests/Support/Creation/CreationContextFactoryTest.php +++ b/tests/Support/Creation/CreationContextFactoryTest.php @@ -98,8 +98,7 @@ SimpleData::class )->withCast('string', StringToUpperCast::class); - $dataClass = new class extends Data - { + $dataClass = new class () extends Data { public string $string; }; @@ -110,15 +109,14 @@ ->findCastForValue($dataProperty)->toBeInstanceOf(StringToUpperCast::class); }); -it('is possible to add a cast collection', function (){ +it('is possible to add a cast collection', function () { $context = CreationContextFactory::createFromConfig(SimpleData::class) ->withCast(\Illuminate\Support\Stringable::class, StringToUpperCast::class) ->withCastCollection(new GlobalCastsCollection([ - 'string' => new StringToUpperCast() + 'string' => new StringToUpperCast(), ])); - $dataClass = new class extends Data - { + $dataClass = new class () extends Data { public string $string; }; diff --git a/tests/Support/Transformation/TransformationContextFactoryTest.php b/tests/Support/Transformation/TransformationContextFactoryTest.php index dcb3e3a9..dc8c5e36 100644 --- a/tests/Support/Transformation/TransformationContextFactoryTest.php +++ b/tests/Support/Transformation/TransformationContextFactoryTest.php @@ -1,6 +1,5 @@ Date: Tue, 23 Jan 2024 14:50:25 +0100 Subject: [PATCH 106/124] Small fixes --- docs/advanced-usage/pipeline.md | 2 +- docs/as-a-resource/transformers.md | 18 +++++++++++++++--- .../DataCollectableFromSomethingResolver.php | 2 +- src/Resolvers/DataFromSomethingResolver.php | 2 +- src/Support/Creation/CreationContext.php | 2 +- .../Creation/CreationContextFactory.php | 2 +- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/docs/advanced-usage/pipeline.md b/docs/advanced-usage/pipeline.md index f3f1aafb..99b02db0 100644 --- a/docs/advanced-usage/pipeline.md +++ b/docs/advanced-usage/pipeline.md @@ -63,7 +63,7 @@ The `handle` method has several arguments: - **dataClass** the data class which is being created - **validationStrategy** the validation strategy which is being used - **mapPropertyNames** whether property names should be mapped - - **withoutMagicalCreation** whether to use the magical creation methods or not + - **disableMagicalCreation** whether to use the magical creation methods or not - **ignoredMagicalMethods** the magical methods which are ignored - **casts** a collection of global casts diff --git a/docs/as-a-resource/transformers.md b/docs/as-a-resource/transformers.md index 1d678e60..6b5e2609 100644 --- a/docs/as-a-resource/transformers.md +++ b/docs/as-a-resource/transformers.md @@ -101,7 +101,7 @@ It is possible to disable the transformation of values, which will make the `tra use Spatie\LaravelData\Support\Transformation\TransformationContext; ArtistData::from($artist)->transform( - TransformationContextFactory::create()->transformValues(false) + TransformationContextFactory::create()->withoutValueTransformation() ); ``` @@ -118,7 +118,7 @@ The [mapping of property names](/docs/laravel-data/v4/as-a-resource/mapping-prop ```php ArtistData::from($artist)->transform( - TransformationContextFactory::create()->mapPropertyNames(false) + TransformationContextFactory::create()->withoutPropertyNameMapping() ); ``` @@ -128,7 +128,7 @@ It is possible to enable [wrapping](/docs/laravel-data/v4/as-a-resource/wrapping use Spatie\LaravelData\Support\Wrapping\WrapExecutionType; ArtistData::from($artist)->transform( - TransformationContextFactory::create()->wrapExecutionType(WrapExecutionType::Enabled) + TransformationContextFactory::create()->withWrapping() ); ``` @@ -142,3 +142,15 @@ Outputting the following array: ], ] ``` + +You can also add additional global transformers as such: + +```php +ArtistData::from($artist)->transform( + TransformationContextFactory::create()->withGlobalTransformer( + 'string', + StringToUpperTransformer::class + ) +); +``` + diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index d98942e5..7457c002 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -74,7 +74,7 @@ protected function createFromCustomCreationMethod( mixed $items, ?string $into, ): null|array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { - if ($creationContext->withoutMagicalCreation) { + if ($creationContext->disableMagicalCreation) { return null; } diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 17d0d9aa..fb9d60d0 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -51,7 +51,7 @@ protected function createFromCustomCreationMethod( CreationContext $creationContext, array $payloads ): ?BaseData { - if ($creationContext->withoutMagicalCreation) { + if ($creationContext->disableMagicalCreation) { return null; } diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php index 570b4592..917dce5a 100644 --- a/src/Support/Creation/CreationContext.php +++ b/src/Support/Creation/CreationContext.php @@ -30,7 +30,7 @@ public function __construct( public string $dataClass, public readonly ValidationStrategy $validationStrategy, public readonly bool $mapPropertyNames, - public readonly bool $withoutMagicalCreation, + public readonly bool $disableMagicalCreation, public readonly ?array $ignoredMagicalMethods, public readonly ?GlobalCastsCollection $casts, ) { diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 4dad0b1e..7fb01eef 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -157,7 +157,7 @@ public function get(): CreationContext dataClass: $this->dataClass, validationStrategy: $this->validationStrategy, mapPropertyNames: $this->mapPropertyNames, - withoutMagicalCreation: $this->disableMagicalCreation, + disableMagicalCreation: $this->disableMagicalCreation, ignoredMagicalMethods: $this->ignoredMagicalMethods, casts: $this->casts, ); From 1ef1f3c7a353251fa5206ad033173f31fc876e63 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 23 Jan 2024 15:00:58 +0100 Subject: [PATCH 107/124] Small fixes --- docs/advanced-usage/creating-a-cast.md | 17 +++++++--- .../creating-a-rule-inferrer.md | 8 ++++- docs/advanced-usage/custom-collections.md | 31 ------------------- src/RuleInferrers/RuleInferrer.php | 6 +--- 4 files changed, 21 insertions(+), 41 deletions(-) delete mode 100644 docs/advanced-usage/custom-collections.md diff --git a/docs/advanced-usage/creating-a-cast.md b/docs/advanced-usage/creating-a-cast.md index be6e0912..31f656aa 100644 --- a/docs/advanced-usage/creating-a-cast.md +++ b/docs/advanced-usage/creating-a-cast.md @@ -10,15 +10,24 @@ A cast implements the following interface: ```php interface Cast { - public function cast(DataProperty $property, mixed $value, array $context): mixed; + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed; } ``` -The value that should be cast is given, and a `DataProperty` object which represents the property for which the value is cast. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures). +A cast receives the following: -Within the `context` array the complete payload is given. +- **property** a `DataProperty` object which represents the property for which the value is cast. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures) +- **value** the value that should be cast +- **properties** an array of the current properties that will be used to create the data object +- **creationContext** the context in which the data object is being created you'll find the following info here: + - **dataClass** the data class which is being created + - **validationStrategy** the validation strategy which is being used + - **mapPropertyNames** whether property names should be mapped + - **disableMagicalCreation** whether to use the magical creation methods or not + - **ignoredMagicalMethods** the magical methods which are ignored + - **casts** a collection of global casts -In the end, the cast should return a casted value. Please note that the given value of a cast can never be `null`. +In the end, the cast should return a casted value. When the cast is unable to cast the value, an `Uncastable` object should be returned. diff --git a/docs/advanced-usage/creating-a-rule-inferrer.md b/docs/advanced-usage/creating-a-rule-inferrer.md index 10a39b5f..be6cfa27 100644 --- a/docs/advanced-usage/creating-a-rule-inferrer.md +++ b/docs/advanced-usage/creating-a-rule-inferrer.md @@ -10,12 +10,18 @@ A rule inferrer can be created by implementing the `RuleInferrer` interface: ```php interface RuleInferrer { - public function handle(DataProperty $property, RulesCollection $rules): array; + public function handle(DataProperty $property, PropertyRules $rules, ValidationContext $context): PropertyRules; } ``` A collection of previous inferred rules is given, and a `DataProperty` object which represents the property for which the value is transformed. You can read more about the internal structures of the package [here](/docs/laravel-data/v4/advanced-usage/internal-structures). +The `ValidationContext` is also injected, this contains the following info: + +- **payload** the current payload respective to the data object which is being validated +- **fullPayload** the full payload which is being validated +- **validationPath** the path from the full payload to the current payload + The `RulesCollection` contains all the rules for the property represented as `ValidationRule` objects. You can add new rules to it: diff --git a/docs/advanced-usage/custom-collections.md b/docs/advanced-usage/custom-collections.md deleted file mode 100644 index abb50547..00000000 --- a/docs/advanced-usage/custom-collections.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Custom collections -weight: 12 ---- - -Laravel-data ships with three collections: - -- **DataCollection** (used for collecting arrays with data ) -- **PaginatedDataCollection** (used for collecting paginators with data) -- **CursorPaginatedDataCollection** (used for collecting cursor paginators with data) - -When calling the `collection` method on a data object, one of these collections will be returned depending on the value given to the method. - -It is possible to return a custom collection in such a scenario. First, you must create a new collection class extending from `DataCollection`, `PaginatedDataCollection`, or `CursorPaginatedDataCollection` depending on what kind of collection you want to build. You can add methods and properties, but the constructor should keep the same signature. - -Next, you need to define that your custom collection should be used when collecting data. Which can be done by setting one of the collection properties in your data object: - -```php -class SongData extends Data -{ - protected static string $_collectionClass = MyCustomDataCollection::class; - - public function __construct( - public string $title, - public string $artist, - ) { - } -} -``` - -For a `PaginatedDataCollection`, you need to set the `$_paginatedCollectionClass` property, and for a `CursorPaginatedDataCollection` the `$_cursorPaginatedCollectionClass` property. diff --git a/src/RuleInferrers/RuleInferrer.php b/src/RuleInferrers/RuleInferrer.php index acc9fefd..8ad05016 100644 --- a/src/RuleInferrers/RuleInferrer.php +++ b/src/RuleInferrers/RuleInferrer.php @@ -8,9 +8,5 @@ interface RuleInferrer { - public function handle( - DataProperty $property, - PropertyRules $rules, - ValidationContext $context, - ): PropertyRules; + public function handle(DataProperty $property, PropertyRules $rules, ValidationContext $context): PropertyRules; } From d70d51af050c44e55f08179a9c7973ef6e02e4bc Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 23 Jan 2024 16:12:25 +0100 Subject: [PATCH 108/124] Cleanup --- UPGRADING.md | 4 + docs/advanced-usage/creating-a-cast.md | 33 +++++++ docs/advanced-usage/creating-a-transformer.md | 33 +++++++ docs/advanced-usage/internal-structures.md | 85 ++++++++++++++++--- docs/advanced-usage/traits-and-interfaces.md | 60 +++++++++++++ .../creating-a-data-object.md | 1 - docs/as-a-data-transfer-object/factories.md | 24 ++++++ src/Attributes/DataCollectionOf.php | 2 +- src/Concerns/EnumerableMethods.php | 2 - src/Contracts/DataCollectable.php | 17 ---- src/Contracts/DataObject.php | 9 -- src/CursorPaginatedDataCollection.php | 12 ++- src/Data.php | 12 ++- src/DataCollection.php | 11 ++- src/PaginatedDataCollection.php | 12 ++- src/Resource.php | 2 +- src/Support/DataClass.php | 30 ++++--- src/Support/Factories/DataClassFactory.php | 9 +- src/Support/Partials/PartialType.php | 8 +- src/Support/VarDumper/DataVarDumperCaster.php | 7 +- src/Support/VarDumper/VarDumperManager.php | 8 +- tests/DataTest.php | 11 ++- tests/Support/DataPropertyTypeTest.php | 1 - tests/Support/DataReturnTypeTest.php | 36 +++----- 24 files changed, 327 insertions(+), 102 deletions(-) create mode 100644 docs/advanced-usage/traits-and-interfaces.md delete mode 100644 src/Contracts/DataCollectable.php delete mode 100644 src/Contracts/DataObject.php diff --git a/UPGRADING.md b/UPGRADING.md index f941c40d..9294ebac 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -161,6 +161,10 @@ object, these have now been removed. Instead, you should use the new DataContext If you were using the `DataCollectableTransformer` or `DataTransformer` then please use the `TransformedDataCollectableResolver` and `TransformedDataResolver` instead. +**Removal of `DataObject` and `DataCollectable` (Likelihood Of Impact: Low)** + +If you were using the `DataObject` or `DataCollectable` interfaces then please replace the interfaces based upon the `Data` and `DataCollection` interfaces to your preference. + **Some advice with this new version of laravel-data** We advise you to take a look at the following things: diff --git a/docs/advanced-usage/creating-a-cast.md b/docs/advanced-usage/creating-a-cast.md index 31f656aa..5f8dbec3 100644 --- a/docs/advanced-usage/creating-a-cast.md +++ b/docs/advanced-usage/creating-a-cast.md @@ -83,3 +83,36 @@ class Email implements Castable } } ``` + +## Combining casts and transformers + +You can combine casts and transformers in one class: + +```php +class ToUpperCastAndTransformer implements Cast, Transformer +{ + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): string + { + return strtoupper($value); + } + + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string + { + return strtoupper($value); + } +} +``` + +Within your data object, you can use the `WithCastAndTransform` attribute to use the cast and transformer: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[WithCastAndTransform(SomeCastAndTransformer::class)] + public string $artist, + ) { + } +} +``` diff --git a/docs/advanced-usage/creating-a-transformer.md b/docs/advanced-usage/creating-a-transformer.md index 397ec835..7c9b06db 100644 --- a/docs/advanced-usage/creating-a-transformer.md +++ b/docs/advanced-usage/creating-a-transformer.md @@ -25,3 +25,36 @@ The following parameters are provided: - **transformers** a collection of transformers that can be used to transform values In the end, the transformer should return a transformed value. + +## Combining transformers and casts + +You can transformers and casts in one class: + +```php +class ToUpperCastAndTransformer implements Cast, Transformer +{ + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): string + { + return strtoupper($value); + } + + public function transform(DataProperty $property, mixed $value, TransformationContext $context): string + { + return strtoupper($value); + } +} +``` + +Within your data object, you can use the `WithCastAndTransform` attribute to use the cast and transformer: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[WithCastAndTransform(SomeCastAndTransformer::class)] + public string $artist, + ) { + } +} +``` diff --git a/docs/advanced-usage/internal-structures.md b/docs/advanced-usage/internal-structures.md index b9b7cf54..f1a0a9ac 100644 --- a/docs/advanced-usage/internal-structures.md +++ b/docs/advanced-usage/internal-structures.md @@ -3,7 +3,8 @@ title: Internal structures weight: 11 --- -This package has some internal structures which are used to analyze data objects and their properties. They can be helpful when writing casts, transformers or rule inferrers. +This package has some internal structures which are used to analyze data objects and their properties. They can be +helpful when writing casts, transformers or rule inferrers. ## DataClass @@ -13,6 +14,23 @@ The DataClass represents the structure of a data object and has the following pr - `properties` all the `DataProperty`'s of the class (more on that later) - `methods` all the magical creation `DataMethod`s of the class (more on that later) - `constructorMethod` the constructor `DataMethod` of the class +- `isReadOnly` is the class read only +- `isAbstract` is the class abstract +- `appendable` is the class implementing `AppendableData` +- `includeable` is the class implementing `IncludeableData` +- `responsable` is the class implementing `ResponsableData` +- `transformable` is the class implementing `TransformableData` +- `validatable` is the class implementing `ValidatableData` +- `wrappable` is the class implementing `WrappableData` +- `emptyData` the the class implementing `EmptyData` +- `attributes` a collection of resolved attributes assigned to the class +- `dataCollectablePropertyAnnotations` the property annotations of the class used to infer the data collection type +- `allowedRequestIncludes` the allowed request includes of the class +- `allowedRequestExcludes` the allowed request excludes of the class +- `allowedRequestOnly` the allowed request only of the class +- `allowedRequestExcept` the allowed request except of the class +- `outputMappedProperties` properties names which are mapped when transforming the data object +- `transformationFields` structure of the transformation fields ## DataProperty @@ -20,44 +38,89 @@ A data property represents a single property within a data object. - `name` the name of the property - `className` the name of the class of the property -- `type` the `DataType` of the property (more on that later) -- `validate` should the property be automatically validated +- `type` the `DataPropertyType` of the property (more on that later) +- `validate` should the property be automatically validated +- `computed` is the property computed +- `hidden` will the property be hidden when transforming the data object - `isPromoted` is the property constructor promoted +- `isReadOnly` is the property read only - `hasDefaultValue` has the property a default value - `defaultValue` the default value of the property - `cast` the cast assigned to the property - `transformer` the transformer assigned to the property - `inputMappedName` the name used to map a property name given - `outputMappedName` the name used to map a property name onto -- `attributes` a collection of `ReflectionAttribute`s assigned to the property +- `attributes` a collection of resolved attributes assigned to the property ## DataMethod A data method represents a method within a data object. - `name` the name of the method -- `parameters` all the `DataParameter`'s of the class (more on that later) +- `parameters` all the `DataParameter`'s and `DataProperty`s of the method (more on that later) - `isStatic` whether the method is static - `isPublic` whether the method is public - `isCustomCreationMethod` whether the method is a custom creation method (=magical creation method) +- `returnType` the `DataType` of the return value (more on that later) ## DataParameter A data parameter represents a single parameter/property within a data method. - `name` the name of the parameter +- `isPromoted` is the property/parameter constructor promoted - `hasDefaultValue` has the parameter a default value - `defaultValue` the default value of the parameter -- `isPromoted` is the property/parameter constructor promoted - `type` the `DataType` of the parameter (more on that later) ## DataType + +A data type represents a type within a data object. + +- `Type` can be a `NamedType`, `UnionType` or `IntersectionType` (more on that later) - `isNullable` can the type be nullable - `isMixed` is the type a mixed type -- `isLazy` can the type be lazy +- `kind` the `DataTypeKind` of the type (more on that later) + +## DataPropertyType + +Extends from the `DataType` and has the following additional properties: + - `isOptional` can the type be optional -- `isDataObject` is the type a data object -- `isDataCollectable` is the type a data collection -- `dataClass` the class of the data object/collection -- `acceptedTypes` an array of types accepted by this type + their base types +- `lazyType` the class of the lazy type for the property +- `dataClass` the data object class of the property or the data object class of the collection it collects +- `dataCollectableClass` the collectable type of the data objects +- `kind` the `DataTypeKind` of the type (more on that later) + +## DataTypeKind + +An enum representing the kind of type of a property/parameter with respect to the package: + +- Default: a non package spefic type +- DataObject: a data object +- DataCollection: a `DataCollection` of data objects +- DataPaginatedCollection: a `DataPaginatedCollection` of data objects +- DataCursorPaginatedCollection: a `DataCursorPaginatedCollection` of data objects +- DataArray: an array of data objects +- DataEnumerable: a `Enumerable` of data objects +- DataPaginator: a `Paginator` of data objects +- DataCursorPaginator: a `CursorPaginator` of data objects + +## NamedType + +Represents a named PHP type with the following properties: + +- `name` the name of the type +- `builtIn` is the type a built-in type +- `acceptedTypes` an array of accepted types as string +- `kind` the `DataTypeKind` of the type (more on that later) +- `dataClass` the data object class of the property or the data object class of the collection it collects +- `dataCollectableClass` the collectable type of the data objects +- `isCastable` wetter the type is a `Castable` + +## UnionType / IntersectionType + +Represents a union or intersection of types with the following properties: + +- `types` an array of types (can be `NamedType`, `UnionType` or `IntersectionType`) diff --git a/docs/advanced-usage/traits-and-interfaces.md b/docs/advanced-usage/traits-and-interfaces.md new file mode 100644 index 00000000..b6a02e84 --- /dev/null +++ b/docs/advanced-usage/traits-and-interfaces.md @@ -0,0 +1,60 @@ +--- +title: Traits and interfaces +weight: 17 +--- + +Laravel data, is built to be as flexible as possible. This means that you can use it in any way you want. + +For example, the `Data` class we've been using throughout these docs is a class implementing a few data interfaces and traits: + +```php +use Illuminate\Contracts\Support\Responsable; +use Spatie\LaravelData\Concerns\AppendableData; +use Spatie\LaravelData\Concerns\BaseData; +use Spatie\LaravelData\Concerns\ContextableData; +use Spatie\LaravelData\Concerns\EmptyData; +use Spatie\LaravelData\Concerns\IncludeableData; +use Spatie\LaravelData\Concerns\ResponsableData; +use Spatie\LaravelData\Concerns\TransformableData; +use Spatie\LaravelData\Concerns\ValidateableData; +use Spatie\LaravelData\Concerns\WrappableData; +use Spatie\LaravelData\Contracts\AppendableData as AppendableDataContract; +use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; +use Spatie\LaravelData\Contracts\EmptyData as EmptyDataContract; +use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; +use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; +use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; +use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; +use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; + +abstract class Data implements Responsable, AppendableDataContract, BaseDataContract, TransformableDataContract, IncludeableDataContract, ResponsableDataContract, ValidateableDataContract, WrappableDataContract, EmptyDataContract +{ + use ResponsableData; + use IncludeableData; + use AppendableData; + use ValidateableData; + use WrappableData; + use TransformableData; + use BaseData; + use EmptyData; + use ContextableData; +} +``` + +These traits and interfaces allow you to create your own versions of the base `Data` class, and add your own functionality to it. + +An example of such custom base data classes are the `Resource` and `Dto` class. + +Each interface (and corresponding trait) provides a piece of functionality: + +- **BaseData** provides the base functionality of the data package to create data objects +- **BaseDataCollectable** provides the base functionality of the data package to create data collections +- **ContextableData** provides the functionality to add context for includes and wraps to the data object/collectable +- **IncludeableData** provides the functionality to add includes, excludes, only and except to the data object/collectable +- **TransformableData** provides the functionality to transform the data object/collectable +- **ResponsableData** provides the functionality to return the data object/collectable as a response +- **WrappableData** provides the functionality to wrap the transformed data object/collectable +- **AppendableData** provides the functionality to append data to the transformed data payload +- **EmptyData** provides the functionality to get an empty version of the data object +- **ValidateableData** provides the functionality to validate the data object +- **DeprecatableData** provides the functionality to add deprecated functionality to the data object diff --git a/docs/as-a-data-transfer-object/creating-a-data-object.md b/docs/as-a-data-transfer-object/creating-a-data-object.md index a097ed27..9999be74 100644 --- a/docs/as-a-data-transfer-object/creating-a-data-object.md +++ b/docs/as-a-data-transfer-object/creating-a-data-object.md @@ -211,4 +211,3 @@ class SongData extends Dto The `Dto` class is a data class in its most basic form. It can br created from anything using magical methods, can validate payloads before creating the data object and can be created using factories. But it doesn't have any of the other functionality that the `Data` class has. - diff --git a/docs/as-a-data-transfer-object/factories.md b/docs/as-a-data-transfer-object/factories.md index dde8887d..2df127ec 100644 --- a/docs/as-a-data-transfer-object/factories.md +++ b/docs/as-a-data-transfer-object/factories.md @@ -71,3 +71,27 @@ SongData::factory()->withCast('string', StringToUpperCast::class)->from(['title' These casts will not replace the other global casts defined in the `data.php` config file, they will though run before the other global casts. You define them just like you would define them in the config file, the first parameter is the type of the property that should be cast and the second parameter is the cast class. + +## Using the creation context + +Internally the package uses a creation context to create data objects. The factory allows you to use this context manually, but when using the from method it will be used automatically. + +It is possible to inject the creation context into a magical method by adding it as a parameter: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function fromModel(Song $song, CreationContext $context): self + { + // Do something with the context + } +} +``` + +You can read more about creation contexts [here](/docs/laravel-data/v4/advanced-usage/pipeline.md). diff --git a/src/Attributes/DataCollectionOf.php b/src/Attributes/DataCollectionOf.php index 762588ee..d2474e41 100644 --- a/src/Attributes/DataCollectionOf.php +++ b/src/Attributes/DataCollectionOf.php @@ -14,7 +14,7 @@ public function __construct( public string $class ) { if (! is_subclass_of($this->class, BaseData::class)) { - throw new CannotFindDataClass("Class {$this->class} given does not implement `DataObject::class`"); + throw new CannotFindDataClass("Class {$this->class} given does not implement `BaseData::class`"); } } } diff --git a/src/Concerns/EnumerableMethods.php b/src/Concerns/EnumerableMethods.php index a2eac9b5..a35bd680 100644 --- a/src/Concerns/EnumerableMethods.php +++ b/src/Concerns/EnumerableMethods.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Concerns; -use Spatie\LaravelData\Contracts\DataCollectable; use Spatie\LaravelData\DataCollection; /** @@ -10,7 +9,6 @@ * @template TValue * * @implements \ArrayAccess - * @implements DataCollectable */ trait EnumerableMethods { diff --git a/src/Contracts/DataCollectable.php b/src/Contracts/DataCollectable.php deleted file mode 100644 index ed4ececf..00000000 --- a/src/Contracts/DataCollectable.php +++ /dev/null @@ -1,17 +0,0 @@ - - */ -interface DataCollectable extends Responsable, BaseDataCollectable, ResponsableData, TransformableData, IncludeableData, WrappableData, IteratorAggregate, Countable -{ -} diff --git a/src/Contracts/DataObject.php b/src/Contracts/DataObject.php deleted file mode 100644 index 7dfe9916..00000000 --- a/src/Contracts/DataObject.php +++ /dev/null @@ -1,9 +0,0 @@ - + * @implements IteratorAggregate */ -class CursorPaginatedDataCollection implements DataCollectable +class CursorPaginatedDataCollection implements Responsable, BaseDataCollectableContract, TransformableDataContract, ResponsableDataContract, IncludeableDataContract, WrappableDataContract, IteratorAggregate, Countable { use ResponsableData; use IncludeableData; diff --git a/src/Data.php b/src/Data.php index a6c4b43e..1a661ab5 100644 --- a/src/Data.php +++ b/src/Data.php @@ -2,6 +2,7 @@ namespace Spatie\LaravelData; +use Illuminate\Contracts\Support\Responsable; use Spatie\LaravelData\Concerns\AppendableData; use Spatie\LaravelData\Concerns\BaseData; use Spatie\LaravelData\Concerns\ContextableData; @@ -11,9 +12,16 @@ use Spatie\LaravelData\Concerns\TransformableData; use Spatie\LaravelData\Concerns\ValidateableData; use Spatie\LaravelData\Concerns\WrappableData; -use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\AppendableData as AppendableDataContract; +use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; +use Spatie\LaravelData\Contracts\EmptyData as EmptyDataContract; +use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; +use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; +use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; +use Spatie\LaravelData\Contracts\ValidateableData as ValidateableDataContract; +use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; -abstract class Data implements DataObject +abstract class Data implements Responsable, AppendableDataContract, BaseDataContract, TransformableDataContract, IncludeableDataContract, ResponsableDataContract, ValidateableDataContract, WrappableDataContract, EmptyDataContract { use ResponsableData; use IncludeableData; diff --git a/src/DataCollection.php b/src/DataCollection.php index d4bf71d0..5188f76d 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -3,8 +3,11 @@ namespace Spatie\LaravelData; use ArrayAccess; +use Countable; +use Illuminate\Contracts\Support\Responsable; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; +use IteratorAggregate; use Spatie\LaravelData\Concerns\BaseDataCollectable; use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\EnumerableMethods; @@ -13,8 +16,12 @@ use Spatie\LaravelData\Concerns\TransformableData; use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; use Spatie\LaravelData\Contracts\DataCollectable; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; +use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; +use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; +use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; use Spatie\LaravelData\Exceptions\CannotCastData; use Spatie\LaravelData\Exceptions\InvalidDataCollectionOperation; use Spatie\LaravelData\Support\EloquentCasts\DataCollectionEloquentCast; @@ -24,9 +31,9 @@ * @template TValue * * @implements \ArrayAccess - * @implements DataCollectable + * @implements IteratorAggregate */ -class DataCollection implements DataCollectable, ArrayAccess +class DataCollection implements Responsable, BaseDataCollectableContract, TransformableDataContract, ResponsableDataContract, IncludeableDataContract, WrappableDataContract, IteratorAggregate, Countable, ArrayAccess { /** @use \Spatie\LaravelData\Concerns\BaseDataCollectable */ use BaseDataCollectable; diff --git a/src/PaginatedDataCollection.php b/src/PaginatedDataCollection.php index 0785b556..b3a5bc2b 100644 --- a/src/PaginatedDataCollection.php +++ b/src/PaginatedDataCollection.php @@ -3,14 +3,22 @@ namespace Spatie\LaravelData; use Closure; +use Countable; use Illuminate\Contracts\Pagination\Paginator; +use Illuminate\Contracts\Support\Responsable; +use IteratorAggregate; use Spatie\LaravelData\Concerns\BaseDataCollectable; use Spatie\LaravelData\Concerns\ContextableData; use Spatie\LaravelData\Concerns\IncludeableData; use Spatie\LaravelData\Concerns\ResponsableData; use Spatie\LaravelData\Concerns\TransformableData; use Spatie\LaravelData\Concerns\WrappableData; +use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; use Spatie\LaravelData\Contracts\DataCollectable; +use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; +use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; +use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; +use Spatie\LaravelData\Contracts\WrappableData as WrappableDataContract; use Spatie\LaravelData\Exceptions\CannotCastData; use Spatie\LaravelData\Exceptions\PaginatedCollectionIsAlwaysWrapped; use Spatie\LaravelData\Support\EloquentCasts\DataCollectionEloquentCast; @@ -19,9 +27,9 @@ * @template TKey of array-key * @template TValue * - * @implements DataCollectable + * @implements IteratorAggregate */ -class PaginatedDataCollection implements DataCollectable +class PaginatedDataCollection implements Responsable, BaseDataCollectableContract, TransformableDataContract, ResponsableDataContract, IncludeableDataContract, WrappableDataContract, IteratorAggregate, Countable { use ResponsableData; use IncludeableData; diff --git a/src/Resource.php b/src/Resource.php index 9a003d05..045bed7b 100644 --- a/src/Resource.php +++ b/src/Resource.php @@ -22,7 +22,7 @@ use Spatie\LaravelData\DataPipes\FillRouteParameterPropertiesDataPipe; use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; -class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, ResponsableDataContract, TransformableDataContract, WrappableDataContract, EmptyDataContract +class Resource implements BaseDataContract, AppendableDataContract, IncludeableDataContract, TransformableDataContract, ResponsableDataContract, WrappableDataContract, EmptyDataContract { use BaseData; use AppendableData; diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 2c7194c5..ab26ced3 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -3,10 +3,9 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; -use Spatie\LaravelData\Contracts\DataObject; /** - * @property class-string $name + * @property class-string $name * @property Collection $properties * @property Collection $methods * @property Collection $attributes @@ -30,10 +29,10 @@ public function __construct( public readonly bool $emptyData, public readonly Collection $attributes, public readonly array $dataCollectablePropertyAnnotations, - public readonly ?array $allowedRequestIncludes, - public readonly ?array $allowedRequestExcludes, - public readonly ?array $allowedRequestOnly, - public readonly ?array $allowedRequestExcept, + public DataStructureProperty $allowedRequestIncludes, + public DataStructureProperty $allowedRequestExcludes, + public DataStructureProperty $allowedRequestOnly, + public DataStructureProperty $allowedRequestExcept, public DataStructureProperty $outputMappedProperties, public DataStructureProperty $transformationFields ) { @@ -41,12 +40,21 @@ public function __construct( public function prepareForCache(): void { - if($this->outputMappedProperties instanceof LazyDataStructureProperty) { - $this->outputMappedProperties = $this->outputMappedProperties->toDataStructureProperty(); - } + $properties = [ + 'allowedRequestIncludes', + 'allowedRequestExcludes', + 'allowedRequestOnly', + 'allowedRequestExcept', + 'outputMappedProperties', + 'transformationFields', + ]; + + foreach ($properties as $propertyName) { + $property = $this->$propertyName; - if($this->transformationFields instanceof LazyDataStructureProperty) { - $this->transformationFields = $this->transformationFields->toDataStructureProperty(); + if ($property instanceof LazyDataStructureProperty) { + $this->$propertyName = $property->toDataStructureProperty(); + } } } } diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php index d233691f..64c739d4 100644 --- a/src/Support/Factories/DataClassFactory.php +++ b/src/Support/Factories/DataClassFactory.php @@ -22,6 +22,7 @@ use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\DataStructureProperty; use Spatie\LaravelData\Support\LazyDataStructureProperty; class DataClassFactory @@ -91,10 +92,10 @@ public function build(ReflectionClass $reflectionClass): DataClass emptyData: $reflectionClass->implementsInterface(EmptyData::class), attributes: $attributes, dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, - allowedRequestIncludes: $responsable ? $name::allowedRequestIncludes() : null, - allowedRequestExcludes: $responsable ? $name::allowedRequestExcludes() : null, - allowedRequestOnly: $responsable ? $name::allowedRequestOnly() : null, - allowedRequestExcept: $responsable ? $name::allowedRequestExcept() : null, + allowedRequestIncludes: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestIncludes() : null), + allowedRequestExcludes: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestExcludes() : null), + allowedRequestOnly: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestOnly() : null), + allowedRequestExcept: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestExcept() : null), outputMappedProperties: $outputMappedProperties, transformationFields: static::resolveTransformationFields($properties), ); diff --git a/src/Support/Partials/PartialType.php b/src/Support/Partials/PartialType.php index 5cdac3ee..745c15bf 100644 --- a/src/Support/Partials/PartialType.php +++ b/src/Support/Partials/PartialType.php @@ -37,10 +37,10 @@ public function getVerb(): string public function getAllowedPartials(DataClass $dataClass): ?array { return match ($this) { - self::Include => $dataClass->allowedRequestIncludes, - self::Exclude => $dataClass->allowedRequestExcludes, - self::Only => $dataClass->allowedRequestOnly, - self::Except => $dataClass->allowedRequestExcept, + self::Include => $dataClass->allowedRequestIncludes->resolve(), + self::Exclude => $dataClass->allowedRequestExcludes->resolve(), + self::Only => $dataClass->allowedRequestOnly->resolve(), + self::Except => $dataClass->allowedRequestExcept->resolve(), }; } } diff --git a/src/Support/VarDumper/DataVarDumperCaster.php b/src/Support/VarDumper/DataVarDumperCaster.php index d427f6eb..163d3daa 100644 --- a/src/Support/VarDumper/DataVarDumperCaster.php +++ b/src/Support/VarDumper/DataVarDumperCaster.php @@ -2,18 +2,17 @@ namespace Spatie\LaravelData\Support\VarDumper; -use Spatie\LaravelData\Contracts\DataCollectable; -use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\TransformableData; use Symfony\Component\VarDumper\Cloner\Stub; class DataVarDumperCaster { - public static function castDataObject(DataObject $data, array $a, Stub $stub, bool $isNested) + public static function castDataObject(TransformableData $data, array $a, Stub $stub, bool $isNested) { return $data->all(); } - public static function castDataCollectable(DataCollectable $data, array $a, Stub $stub, bool $isNested) + public static function castDataCollectable(TransformableData $data, array $a, Stub $stub, bool $isNested) { return [ 'items' => $data->all(), diff --git a/src/Support/VarDumper/VarDumperManager.php b/src/Support/VarDumper/VarDumperManager.php index 739eca91..d8d682c9 100644 --- a/src/Support/VarDumper/VarDumperManager.php +++ b/src/Support/VarDumper/VarDumperManager.php @@ -2,15 +2,15 @@ namespace Spatie\LaravelData\Support\VarDumper; -use Spatie\LaravelData\Contracts\DataCollectable; -use Spatie\LaravelData\Contracts\DataObject; +use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\BaseDataCollectable; use Symfony\Component\VarDumper\Cloner\AbstractCloner; class VarDumperManager { public function initialize(): void { - AbstractCloner::$defaultCasters[DataObject::class] = [DataVarDumperCaster::class, 'castDataObject']; - AbstractCloner::$defaultCasters[DataCollectable::class] = [DataVarDumperCaster::class, 'castDataCollectable']; + AbstractCloner::$defaultCasters[BaseData::class] = [DataVarDumperCaster::class, 'castDataObject']; + AbstractCloner::$defaultCasters[BaseDataCollectable::class] = [DataVarDumperCaster::class, 'castDataCollectable']; } } diff --git a/tests/DataTest.php b/tests/DataTest.php index 800e2927..d3d100be 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1,5 +1,6 @@ new DataCollection(SimpleData::class, []), new DataType( type: new NamedType(DataCollection::class, false, [ - DataCollectable::class, + Illuminate\Contracts\Support\Responsable::class, + Spatie\LaravelData\Contracts\BaseDataCollectable::class, + Spatie\LaravelData\Contracts\TransformableData::class, + Spatie\LaravelData\Contracts\ResponsableData::class, + Spatie\LaravelData\Contracts\IncludeableData::class, + Spatie\LaravelData\Contracts\WrappableData::class, + IteratorAggregate::class, + Countable::class, ArrayAccess::class, - Traversable::class, - ContextableData::class, - Castable::class, - Arrayable::class, - Jsonable::class, + Illuminate\Contracts\Database\Eloquent\Castable::class, + Illuminate\Contracts\Support\Arrayable::class, + Illuminate\Contracts\Support\Jsonable::class, JsonSerializable::class, - Countable::class, - IteratorAggregate::class, - WrappableData::class, - IncludeableData::class, - TransformableData::class, - ResponsableData::class, - BaseDataCollectable::class, - Responsable::class, - + Spatie\LaravelData\Contracts\ContextableData::class, + Traversable::class, ], DataTypeKind::DataCollection, null, null), isNullable: false, isMixed: false, From 0d11df7f0c17ace7c2dcfd5be0e7ed0fe22ad32c Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Tue, 23 Jan 2024 15:12:49 +0000 Subject: [PATCH 109/124] Fix styling --- src/CursorPaginatedDataCollection.php | 1 - src/DataCollection.php | 1 - src/PaginatedDataCollection.php | 1 - src/Support/Factories/DataClassFactory.php | 9 ++++----- tests/DataTest.php | 1 - tests/Support/DataPropertyTypeTest.php | 1 - tests/Support/DataReturnTypeTest.php | 1 - 7 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/CursorPaginatedDataCollection.php b/src/CursorPaginatedDataCollection.php index 462fce0f..8bc25f99 100644 --- a/src/CursorPaginatedDataCollection.php +++ b/src/CursorPaginatedDataCollection.php @@ -14,7 +14,6 @@ use Spatie\LaravelData\Concerns\TransformableData; use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; -use Spatie\LaravelData\Contracts\DataCollectable; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; diff --git a/src/DataCollection.php b/src/DataCollection.php index 5188f76d..89666bc0 100644 --- a/src/DataCollection.php +++ b/src/DataCollection.php @@ -17,7 +17,6 @@ use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; -use Spatie\LaravelData\Contracts\DataCollectable; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; diff --git a/src/PaginatedDataCollection.php b/src/PaginatedDataCollection.php index b3a5bc2b..fac3e57f 100644 --- a/src/PaginatedDataCollection.php +++ b/src/PaginatedDataCollection.php @@ -14,7 +14,6 @@ use Spatie\LaravelData\Concerns\TransformableData; use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\BaseDataCollectable as BaseDataCollectableContract; -use Spatie\LaravelData\Contracts\DataCollectable; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; use Spatie\LaravelData\Contracts\TransformableData as TransformableDataContract; diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php index 64c739d4..78fa6d26 100644 --- a/src/Support/Factories/DataClassFactory.php +++ b/src/Support/Factories/DataClassFactory.php @@ -22,7 +22,6 @@ use Spatie\LaravelData\Support\Annotations\DataCollectableAnnotationReader; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\DataStructureProperty; use Spatie\LaravelData\Support\LazyDataStructureProperty; class DataClassFactory @@ -92,10 +91,10 @@ public function build(ReflectionClass $reflectionClass): DataClass emptyData: $reflectionClass->implementsInterface(EmptyData::class), attributes: $attributes, dataCollectablePropertyAnnotations: $dataCollectablePropertyAnnotations, - allowedRequestIncludes: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestIncludes() : null), - allowedRequestExcludes: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestExcludes() : null), - allowedRequestOnly: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestOnly() : null), - allowedRequestExcept: new LazyDataStructureProperty(fn(): ?array => $responsable ? $name::allowedRequestExcept() : null), + allowedRequestIncludes: new LazyDataStructureProperty(fn (): ?array => $responsable ? $name::allowedRequestIncludes() : null), + allowedRequestExcludes: new LazyDataStructureProperty(fn (): ?array => $responsable ? $name::allowedRequestExcludes() : null), + allowedRequestOnly: new LazyDataStructureProperty(fn (): ?array => $responsable ? $name::allowedRequestOnly() : null), + allowedRequestExcept: new LazyDataStructureProperty(fn (): ?array => $responsable ? $name::allowedRequestExcept() : null), outputMappedProperties: $outputMappedProperties, transformationFields: static::resolveTransformationFields($properties), ); diff --git a/tests/DataTest.php b/tests/DataTest.php index d3d100be..47edb71c 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -13,7 +13,6 @@ use Spatie\LaravelData\Concerns\WrappableData; use Spatie\LaravelData\Contracts\AppendableData as AppendableDataContract; use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; -use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Contracts\EmptyData as EmptyDataContract; use Spatie\LaravelData\Contracts\IncludeableData as IncludeableDataContract; use Spatie\LaravelData\Contracts\ResponsableData as ResponsableDataContract; diff --git a/tests/Support/DataPropertyTypeTest.php b/tests/Support/DataPropertyTypeTest.php index b7c6d138..6cf1300e 100644 --- a/tests/Support/DataPropertyTypeTest.php +++ b/tests/Support/DataPropertyTypeTest.php @@ -12,7 +12,6 @@ use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\ContextableData; -use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; use Spatie\LaravelData\Contracts\ResponsableData; diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php index ea8fe90b..3c539491 100644 --- a/tests/Support/DataReturnTypeTest.php +++ b/tests/Support/DataReturnTypeTest.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; -use Spatie\LaravelData\Contracts\DataCollectable; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Enums\DataTypeKind; use Spatie\LaravelData\Support\DataType; From d70d4d55bfc96d07c20bbe11c08f75b4d90a57e2 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Tue, 23 Jan 2024 16:13:52 +0100 Subject: [PATCH 110/124] Fix PHPStan --- phpstan-baseline.neon | 5 ----- 1 file changed, 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 435d719d..73827bcc 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -105,11 +105,6 @@ parameters: count: 1 path: src/PaginatedDataCollection.php - - - message: "#^Dead catch \\- ArgumentCountError is never thrown in the try block\\.$#" - count: 1 - path: src/Resolvers/DataFromArrayResolver.php - - message: "#^PHPDoc tag @var for variable \\$dataClass has invalid type Spatie\\\\LaravelData\\\\Concerns\\\\EmptyData\\.$#" count: 1 From 46e2d19c8bfebe1bca4fc5c7149ab21bac9461fb Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 24 Jan 2024 11:20:07 +0100 Subject: [PATCH 111/124] More test coverage --- src/Attributes/MapName.php | 3 +- src/Concerns/BaseData.php | 8 - src/Concerns/EnumerableMethods.php | 25 +++ src/Contracts/BaseData.php | 2 - src/Exceptions/DataMissingFeature.php | 37 ----- src/Exceptions/InvalidDataClassMapper.php | 22 --- src/LaravelDataServiceProvider.php | 5 - src/Mappers/CamelCaseMapper.php | 4 - src/Mappers/ProvidedNameMapper.php | 5 - src/Mappers/SnakeCaseMapper.php | 4 - src/Mappers/StudlyCaseMapper.php | 4 - .../DataCollectableFromSomethingResolver.php | 4 - src/Resolvers/NameMappersResolver.php | 8 +- .../RequestQueryStringPartialsResolver.php | 2 +- .../DataCollectableAnnotationReader.php | 45 ------ src/Support/DataClassMorphMap.php | 9 +- src/Support/Factories/DataClassFactory.php | 5 +- src/Support/Factories/DataMethodFactory.php | 9 -- src/Support/Factories/DataTypeFactory.php | 4 +- tests/CreationTest.php | 19 +++ tests/DataTest.php | 1 + .../CustomCursorPaginatedDataCollection.php | 10 ++ tests/LivewireTest.php | 2 + tests/MagicalCreationTest.php | 143 +++++++++++++++++- tests/MappingTest.php | 40 +++++ .../Normalizers/FormRequestNormalizerTest.php | 15 +- tests/PartialsTest.php | 40 +++++ tests/Support/DataReturnTypeTest.php | 50 +++++- .../Transformers/ArrayableTransformerTest.php | 27 ++++ tests/ValidationTest.php | 40 +++++ 30 files changed, 422 insertions(+), 170 deletions(-) delete mode 100644 src/Exceptions/DataMissingFeature.php delete mode 100644 src/Exceptions/InvalidDataClassMapper.php create mode 100644 tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php create mode 100644 tests/Transformers/ArrayableTransformerTest.php diff --git a/src/Attributes/MapName.php b/src/Attributes/MapName.php index 316e28cb..d0f8779f 100644 --- a/src/Attributes/MapName.php +++ b/src/Attributes/MapName.php @@ -3,11 +3,12 @@ namespace Spatie\LaravelData\Attributes; use Attribute; +use Spatie\LaravelData\Mappers\NameMapper; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] class MapName { - public function __construct(public string|int $input, public string|int|null $output = null) + public function __construct(public string|int|NameMapper $input, public string|int|NameMapper|null $output = null) { $this->output ??= $this->input; } diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 53b4d618..28ead482 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -85,14 +85,6 @@ public static function prepareForPipeline(array $properties): array return $properties; } - public function getMorphClass(): string - { - /** @var class-string $class */ - $class = static::class; - - return app(DataConfig::class)->morphMap->getDataClassAlias($class) ?? $class; - } - public function __sleep(): array { $dataClass = app(DataConfig::class)->getDataClass(static::class); diff --git a/src/Concerns/EnumerableMethods.php b/src/Concerns/EnumerableMethods.php index a35bd680..b5572bc8 100644 --- a/src/Concerns/EnumerableMethods.php +++ b/src/Concerns/EnumerableMethods.php @@ -13,6 +13,8 @@ trait EnumerableMethods { /** + * @deprecated In v5, use a regular Laravel collection instead + * * @param callable(TValue, TKey): TValue $through * * @return static @@ -27,6 +29,8 @@ public function through(callable $through): static } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @param callable(TValue, TKey): TValue $map * * @return static @@ -37,6 +41,8 @@ public function map(callable $map): static } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @param callable(TValue): bool $filter * * @return static @@ -51,6 +57,8 @@ public function filter(callable $filter): static } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @param callable(TValue): bool $filter * * @return static @@ -65,6 +73,8 @@ public function reject(callable $filter): static } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @template TFirstDefault * * @param null| (callable(TValue,TKey): bool) $callback @@ -78,6 +88,8 @@ public function first(callable|null $callback = null, $default = null) } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @template TLastDefault * * @param null| (callable(TValue,TKey): bool) $callback @@ -91,6 +103,8 @@ public function last(callable|null $callback = null, $default = null) } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @param callable(TValue, TKey): mixed $callback * * @return static @@ -103,6 +117,8 @@ public function each(callable $callback): static } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @return static */ public function values(): static @@ -114,6 +130,9 @@ public function values(): static return $cloned; } + /** + * @deprecated In v5, use a regular Laravel collection instead + */ public function where(string $key, mixed $operator = null, mixed $value = null): static { $cloned = clone $this; @@ -124,6 +143,8 @@ public function where(string $key, mixed $operator = null, mixed $value = null): } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @template TReduceInitial * @template TReduceReturnType * @@ -138,6 +159,8 @@ public function reduce(callable $callback, mixed $initial = null) } /** + * @deprecated In v5, use a regular Laravel collection instead + * * @param (callable(TValue, TKey): bool)|string|null $key * @param mixed $operator * @param mixed $value @@ -153,6 +176,8 @@ public function sole(callable|string|null $key = null, mixed $operator = null, m } /** + * + * * @param DataCollection $collection * * @return static diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 3675e550..2faf9ae8 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -51,6 +51,4 @@ public static function normalizers(): array; public static function prepareForPipeline(array $properties): array; public static function pipeline(): DataPipeline; - - public function getMorphClass(): string; } diff --git a/src/Exceptions/DataMissingFeature.php b/src/Exceptions/DataMissingFeature.php deleted file mode 100644 index d9420263..00000000 --- a/src/Exceptions/DataMissingFeature.php +++ /dev/null @@ -1,37 +0,0 @@ -filter(fn (string $interface) => in_array($interface, class_implements($dataClass))) - ->map(fn (string $interface) => Str::afterLast($interface, '\\')) - ->map(fn (string $interface) => "`{$interface}`") - ->join(', '); - - return new self("Feature `{$featureClass}` missing in data object `{$dataClass}` implementing {$implemented}"); - } -} diff --git a/src/Exceptions/InvalidDataClassMapper.php b/src/Exceptions/InvalidDataClassMapper.php deleted file mode 100644 index 85c3cdf8..00000000 --- a/src/Exceptions/InvalidDataClassMapper.php +++ /dev/null @@ -1,22 +0,0 @@ -className}:{$target->name}" - : $target->name; - - return new self("`MapFrom` attribute on `{$target}` should be a class implementing `{$mapperClass}`"); - } -} diff --git a/src/LaravelDataServiceProvider.php b/src/LaravelDataServiceProvider.php index be487d3c..1143b817 100644 --- a/src/LaravelDataServiceProvider.php +++ b/src/LaravelDataServiceProvider.php @@ -34,12 +34,7 @@ public function packageRegistered(): void fn () => $this->app->make(DataStructureCache::class)->getConfig() ?? DataConfig::createFromConfig(config('data')) ); - /** @psalm-suppress UndefinedInterfaceMethod */ $this->app->beforeResolving(BaseData::class, function ($class, $parameters, $app) { - if ($app->has($class)) { - return; - } - $app->bind( $class, fn ($container) => $class::from($container['request']) diff --git a/src/Mappers/CamelCaseMapper.php b/src/Mappers/CamelCaseMapper.php index e75c67a5..12aaf192 100644 --- a/src/Mappers/CamelCaseMapper.php +++ b/src/Mappers/CamelCaseMapper.php @@ -8,10 +8,6 @@ class CamelCaseMapper implements NameMapper { public function map(int|string $name): string|int { - if (! is_string($name)) { - return $name; - } - return Str::camel($name); } } diff --git a/src/Mappers/ProvidedNameMapper.php b/src/Mappers/ProvidedNameMapper.php index 9a517b88..f003cf44 100644 --- a/src/Mappers/ProvidedNameMapper.php +++ b/src/Mappers/ProvidedNameMapper.php @@ -12,9 +12,4 @@ public function map(int|string $name): string|int { return $this->name; } - - public function inverse(): NameMapper - { - return $this; - } } diff --git a/src/Mappers/SnakeCaseMapper.php b/src/Mappers/SnakeCaseMapper.php index 81da692d..c9c6796d 100644 --- a/src/Mappers/SnakeCaseMapper.php +++ b/src/Mappers/SnakeCaseMapper.php @@ -8,10 +8,6 @@ class SnakeCaseMapper implements NameMapper { public function map(int|string $name): string|int { - if (! is_string($name)) { - return $name; - } - return Str::snake($name); } } diff --git a/src/Mappers/StudlyCaseMapper.php b/src/Mappers/StudlyCaseMapper.php index 56ac998f..0232ea5e 100644 --- a/src/Mappers/StudlyCaseMapper.php +++ b/src/Mappers/StudlyCaseMapper.php @@ -8,10 +8,6 @@ class StudlyCaseMapper implements NameMapper { public function map(int|string $name): string|int { - if (! is_string($name)) { - return $name; - } - return Str::studly($name); } } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 7457c002..2d66231a 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -52,10 +52,6 @@ public function execute( $normalizedItems = $this->normalizeItems($items, $dataClass, $creationContext); - if(! $intoType->type instanceof NamedType) { - throw new Exception('Cannot collect into a union or intersection type'); - } - return match ($intoType->kind) { DataTypeKind::DataArray => $this->normalizeToArray($normalizedItems), DataTypeKind::DataEnumerable => new $intoType->type->name($this->normalizeToArray($normalizedItems)), diff --git a/src/Resolvers/NameMappersResolver.php b/src/Resolvers/NameMappersResolver.php index a85aad0c..c3093d95 100644 --- a/src/Resolvers/NameMappersResolver.php +++ b/src/Resolvers/NameMappersResolver.php @@ -57,7 +57,7 @@ protected function resolveOutputNameMapper( return null; } - protected function resolveMapper(string|int $value): ?NameMapper + protected function resolveMapper(string|int|NameMapper $value): ?NameMapper { $mapper = $this->resolveMapperClass($value); @@ -70,12 +70,16 @@ protected function resolveMapper(string|int $value): ?NameMapper return $mapper; } - protected function resolveMapperClass(int|string $value): NameMapper + protected function resolveMapperClass(int|string|NameMapper $value): NameMapper { if (is_int($value)) { return new ProvidedNameMapper($value); } + if($value instanceof NameMapper){ + return $value; + } + if (is_a($value, NameMapper::class, true)) { return resolve($value); } diff --git a/src/Resolvers/RequestQueryStringPartialsResolver.php b/src/Resolvers/RequestQueryStringPartialsResolver.php index 7a69959c..ead50f69 100644 --- a/src/Resolvers/RequestQueryStringPartialsResolver.php +++ b/src/Resolvers/RequestQueryStringPartialsResolver.php @@ -75,7 +75,7 @@ protected function validateSegments( ): ?array { $allowed = $type->getAllowedPartials($dataClass); - $segment = $partialSegments[0]; + $segment = $partialSegments[0] ?? null; if ($segment instanceof AllPartialSegment) { if ($allowed === null || $allowed === ['*']) { diff --git a/src/Support/Annotations/DataCollectableAnnotationReader.php b/src/Support/Annotations/DataCollectableAnnotationReader.php index 86e4a472..54be1054 100644 --- a/src/Support/Annotations/DataCollectableAnnotationReader.php +++ b/src/Support/Annotations/DataCollectableAnnotationReader.php @@ -23,11 +23,6 @@ class DataCollectableAnnotationReader /** @var array */ protected static array $contexts = []; - public static function create(): self - { - return new self(); - } - /** @return array */ public function getForClass(ReflectionClass $class): array { @@ -172,46 +167,6 @@ protected function resolveDataClass( return null; } - protected function resolveCollectionClass( - ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, - string $class - ): ?string { - if (str_contains($class, '|')) { - foreach (explode('|', $class) as $explodedClass) { - if ($foundClass = $this->resolveCollectionClass($reflection, $explodedClass)) { - return $foundClass; - } - } - - return null; - } - - if ($class === 'array') { - return $class; - } - - $class = ltrim($class, '\\'); - - if (is_a($class, BaseDataCollectable::class, true) - || is_a($class, Enumerable::class, true) - || is_a($class, AbstractPaginator::class, true) - || is_a($class, CursorPaginator::class, true) - ) { - return $class; - } - - $class = $this->resolveFcqn($reflection, $class); - - if (is_a($class, BaseDataCollectable::class, true) - || is_a($class, Enumerable::class, true) - || is_a($class, AbstractPaginator::class, true) - || is_a($class, CursorPaginator::class, true)) { - return $class; - } - - return null; - } - protected function resolveFcqn( ReflectionProperty|ReflectionClass|ReflectionMethod $reflection, string $class diff --git a/src/Support/DataClassMorphMap.php b/src/Support/DataClassMorphMap.php index 660aa02d..d1e604a7 100644 --- a/src/Support/DataClassMorphMap.php +++ b/src/Support/DataClassMorphMap.php @@ -30,15 +30,8 @@ public function add( /** * @param array> $map */ - public function merge(array|DataClassMorphMap $map): self + public function merge(array $map): self { - if ($map instanceof DataClassMorphMap) { - $map->map = array_merge($this->map, $map->map); - $map->reversedMap = array_merge($this->reversedMap, $map->reversedMap); - - return $this; - } - foreach ($map as $alias => $class) { $this->add($alias, $class); } diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php index 78fa6d26..2247a219 100644 --- a/src/Support/Factories/DataClassFactory.php +++ b/src/Support/Factories/DataClassFactory.php @@ -121,7 +121,10 @@ protected function resolveMethods( ): Collection { return collect($reflectionClass->getMethods()) ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'from') || str_starts_with($reflectionMethod->name, 'collect')) - ->reject(fn (ReflectionMethod $reflectionMethod) => in_array($reflectionMethod->name, ['from', 'collect', 'collection'])) + ->reject(fn (ReflectionMethod $reflectionMethod) => in_array($reflectionMethod->name, ['from', 'collect', 'collection']) + || $reflectionMethod->isStatic() === false + || $reflectionMethod->isPublic() === false + ) ->mapWithKeys( fn (ReflectionMethod $reflectionMethod) => [$reflectionMethod->name => $this->methodFactory->build($reflectionMethod, $reflectionClass)], ); diff --git a/src/Support/Factories/DataMethodFactory.php b/src/Support/Factories/DataMethodFactory.php index 6f34fa5d..314d4e63 100644 --- a/src/Support/Factories/DataMethodFactory.php +++ b/src/Support/Factories/DataMethodFactory.php @@ -73,15 +73,6 @@ protected function resolveCustomCreationMethodType( ReflectionMethod $method, ?DataType $returnType, ): CustomCreationMethodType { - if (! $method->isStatic() - || ! $method->isPublic() - || $method->name === 'from' - || $method->name === 'collect' - || $method->name === 'collection' - ) { - return CustomCreationMethodType::None; - } - if (str_starts_with($method->name, 'from')) { return CustomCreationMethodType::Object; } diff --git a/src/Support/Factories/DataTypeFactory.php b/src/Support/Factories/DataTypeFactory.php index 689a6b9d..322240ab 100644 --- a/src/Support/Factories/DataTypeFactory.php +++ b/src/Support/Factories/DataTypeFactory.php @@ -42,7 +42,7 @@ public function __construct( public function buildProperty( ?ReflectionType $reflectionType, ReflectionClass|string $class, - ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ReflectionProperty|ReflectionParameter|string $typeable, ?Collection $attributes = null, ?DataCollectableAnnotation $classDefinedDataCollectableAnnotation = null, ): DataPropertyType { @@ -70,7 +70,7 @@ classDefinedDataCollectableAnnotation: $classDefinedDataCollectableAnnotation, public function build( ?ReflectionType $reflectionType, ReflectionClass|string $class, - ReflectionMethod|ReflectionProperty|ReflectionParameter|string $typeable, + ReflectionProperty|ReflectionParameter|string $typeable, ): DataType { $properties = $this->infer( reflectionType: $reflectionType, diff --git a/tests/CreationTest.php b/tests/CreationTest.php index 44e44d33..8cfe775f 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -3,6 +3,7 @@ use Carbon\Carbon; use Carbon\CarbonImmutable; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Pagination\CursorPaginator; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Validation\ValidationException; use Spatie\LaravelData\Attributes\Computed; @@ -25,6 +26,7 @@ use Spatie\LaravelData\Tests\Fakes\Casts\ContextAwareCast; use Spatie\LaravelData\Tests\Fakes\Casts\StringToUpperCast; use Spatie\LaravelData\Tests\Fakes\ComplicatedData; +use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomCursorPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\EnumData; @@ -183,6 +185,7 @@ }); it('can optionally create data', function () { + expect(SimpleData::optional())->toBeNull(); expect(SimpleData::optional(null))->toBeNull(); expect(new SimpleData('Hello world'))->toEqual( SimpleData::optional(['string' => 'Hello world']) @@ -760,6 +763,22 @@ public function __construct(public string $string) expect($collection)->toBeInstanceOf(CustomPaginatedDataCollection::class); }); +it('can return a custom cursor paginated data collection when collecting data', function () { + $class = new class ('') extends Data implements DeprecatedDataContract { + use WithDeprecatedCollectionMethod; + + protected static string $_cursorPaginatedCollectionClass = CustomCursorPaginatedDataCollection::class; + + public function __construct(public string $string) + { + } + }; + + $collection = $class::collection(new CursorPaginator([['string' => 'A'], ['string' => 'B']], 2)); + + expect($collection)->toBeInstanceOf(CustomCursorPaginatedDataCollection::class); +}); + it('will allow a nested data object to cast properties however it wants', function () { $model = new DummyModel(['id' => 10]); diff --git a/tests/DataTest.php b/tests/DataTest.php index 47edb71c..3c98c93f 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -26,6 +26,7 @@ use Spatie\LaravelData\Tests\Fakes\SimpleDto; use Spatie\LaravelData\Tests\Fakes\SimpleResource; +use Symfony\Component\VarDumper\VarDumper; use function Spatie\Snapshots\assertMatchesSnapshot; it('also works by using traits and interfaces, skipping the base data class', function () { diff --git a/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php b/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php new file mode 100644 index 00000000..202232b4 --- /dev/null +++ b/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php @@ -0,0 +1,10 @@ + 'Freek']); expect($data)->toEqual(new $class('Freek')); + + expect($data->toLivewire())->toEqual(['name' => 'Freek']); }); diff --git a/tests/MagicalCreationTest.php b/tests/MagicalCreationTest.php index edea8a5b..b53f9ba6 100644 --- a/tests/MagicalCreationTest.php +++ b/tests/MagicalCreationTest.php @@ -3,10 +3,16 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Model; +use Illuminate\Pagination\CursorPaginator; +use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; +use Illuminate\Support\LazyCollection; +use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; +use Spatie\LaravelData\PaginatedDataCollection; use Spatie\LaravelData\Support\Creation\CreationContext; +use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataWithMultipleArgumentCreationMethod; use Spatie\LaravelData\Tests\Fakes\DummyDto; use Spatie\LaravelData\Tests\Fakes\EnumData; @@ -195,6 +201,16 @@ public static function collectCollection(Collection $items): array { return $items->all(); } + + public static function collectPaginator(LengthAwarePaginator $items): CursorPaginator + { + return new CursorPaginator($items->all(), $items->perPage()); + } + + public static function collectCursorPaginator(CursorPaginator $items): LengthAwarePaginator + { + return new LengthAwarePaginator($items->all(), $items->count(), $items->perPage()); + } }; expect($dataClass::collect(['a', 'b', 'c'])) @@ -220,6 +236,12 @@ public static function collectCollection(Collection $items): array $dataClass::from('b'), $dataClass::from('c'), ]); + + expect($dataClass::collect(new LengthAwarePaginator(['a', 'b', 'c'], 3, 15))) + ->toBeInstanceOf(CursorPaginator::class); + + expect($dataClass::collect(new CursorPaginator(['a', 'b', 'c'], 15))) + ->toBeInstanceOf(LengthAwarePaginator::class); }); it('can disable magically collecting data', function () { @@ -284,15 +306,128 @@ public static function collectArray(array $items): Collection $dataClass = new class ('') extends SimpleData { public static function collectArray(array $items, CreationContext $context): array { - return array_map(fn (SimpleData $data) => new SimpleData($data->string . ' ' . $context->dataClass), $items); + return array_map(fn (SimpleData $data) => new SimpleData($data->string.' '.$context->dataClass), $items); } }; expect($dataClass::collect(['a', 'b', 'c'])) ->toBeArray() ->toEqual([ - SimpleData::from('a ' . $dataClass::class), - SimpleData::from('b ' . $dataClass::class), - SimpleData::from('c ' . $dataClass::class), + SimpleData::from('a '.$dataClass::class), + SimpleData::from('b '.$dataClass::class), + SimpleData::from('c '.$dataClass::class), ]); }); + +it('can use a string to collect data into', function ( + string $into, + array|object $expected, +) { + expect(SimpleData::collect(['A', 'B'], $into))->toEqual($expected); +})->with(function(){ + yield 'array' => [ + 'array', + fn() => [ + SimpleData::from('A'), + SimpleData::from('B'), + ], + ]; + + yield 'laravel collection' => [ + Collection::class, + fn() => collect([ + SimpleData::from('A'), + SimpleData::from('B'), + ]), + ]; + + yield 'laravel lazy collection' => [ + LazyCollection::class, + fn() =>new LazyCollection([ + SimpleData::from('A'), + SimpleData::from('B'), + ]), + ]; + + yield 'data collection' => [ + DataCollection::class, + fn() => new DataCollection(SimpleData::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]), + ]; + + yield 'data paginated collection' => [ + PaginatedDataCollection::class, + fn() => new PaginatedDataCollection(SimpleData::class,new LengthAwarePaginator( [ + SimpleData::from('A'), + SimpleData::from('B'), + ], 2, 15)), + ]; + + yield 'data cursor paginated collection' => [ + CursorPaginatedDataCollection::class, + fn() => new CursorPaginatedDataCollection(SimpleData::class,new CursorPaginator( [ + SimpleData::from('A'), + SimpleData::from('B'), + ], 15)), + ]; + + yield 'paginator' => [ + LengthAwarePaginator::class, + fn() => new LengthAwarePaginator([ + SimpleData::from('A'), + SimpleData::from('B'), + ], 2, 15), + ]; + + yield 'cursor paginator' => [ + CursorPaginator::class, + fn() => new CursorPaginator([ + SimpleData::from('A'), + SimpleData::from('B'), + ], 15), + ]; + + yield 'custom data collection' => [ + CustomDataCollection::class, + fn() => new CustomDataCollection(SimpleData::class, [ + SimpleData::from('A'), + SimpleData::from('B'), + ]), + ]; +}); + +it('can specifically select the correct collect method using an into return type', function () { + $dataClass = new class ('') extends SimpleData { + public static function collectArray(array $items): array + { + return array_map( + fn (SimpleData $data) => new SimpleData(strtoupper($data->string)), + $items + ); + } + + public static function collectCollection(array $items): Collection + { + return collect(array_map( + fn (SimpleData $data) => new SimpleData(strtolower($data->string)), + $items + )); + } + }; + + expect($dataClass::collect(['Hello', 'World'], 'array')) + ->toBeArray() + ->toEqual([SimpleData::from('HELLO'), SimpleData::from('WORLD')]); + + expect($dataClass::collect(['Hello', 'World'], Collection::class)) + ->toBeInstanceOf(Collection::class) + ->all()->toEqual([SimpleData::from('hello'), SimpleData::from('world')]); +}); + +it('can only collect arrays/collections/paginators', function () { + $storage = new SplObjectStorage(); + + expect(fn () => SimpleData::collect($storage))->toThrow(Exception::class, 'Unable to normalize items'); +}); diff --git a/tests/MappingTest.php b/tests/MappingTest.php index ebc2c5a8..6736b333 100644 --- a/tests/MappingTest.php +++ b/tests/MappingTest.php @@ -2,9 +2,13 @@ use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\MapInputName; +use Spatie\LaravelData\Attributes\MapName; use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Data; +use Spatie\LaravelData\Mappers\CamelCaseMapper; +use Spatie\LaravelData\Mappers\ProvidedNameMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper; +use Spatie\LaravelData\Mappers\StudlyCaseMapper; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Fakes\DataWithMapper; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -312,3 +316,39 @@ public function __construct( 'But not too expensive!', ])); }); + +it('has a mappers built in', function (){ + $data = new class extends Data + { + #[MapName(CamelCaseMapper::class)] + public string $camel_case = 'camelCase'; + + #[MapName(SnakeCaseMapper::class)] + public string $snakeCase = 'snake_case'; + + #[MapName(StudlyCaseMapper::class)] + public string $studly_case = 'StudlyCase'; + + #[MapName(new ProvidedNameMapper('i_provided'))] + public string $provided = 'provided'; + }; + + expect($data->toArray())->toEqual([ + 'camelCase' => 'camelCase', + 'snake_case' => 'snake_case', + 'StudlyCase' => 'StudlyCase', + 'i_provided' => 'provided', + ]); + + expect($data::from([ + 'camelCase' => 'camelCase', + 'snake_case' => 'snake_case', + 'StudlyCase' => 'StudlyCase', + 'i_provided' => 'provided', + ])) + ->camel_case->toBe('camelCase') + ->snakeCase->toBe('snake_case') + ->studly_case->toBe('StudlyCase') + ->provided->toBe('provided'); +}); + diff --git a/tests/Normalizers/FormRequestNormalizerTest.php b/tests/Normalizers/FormRequestNormalizerTest.php index 7c5606c3..eafe6827 100644 --- a/tests/Normalizers/FormRequestNormalizerTest.php +++ b/tests/Normalizers/FormRequestNormalizerTest.php @@ -1,11 +1,24 @@ set('data.normalizers', [FormRequestNormalizer::class]); + config()->set('data.normalizers', [FormRequestNormalizer::class, ArrayNormalizer::class]); +}); + +it('will not normalize any other thing than a FormRequest', function () { + $data = DataWithNullable::from([ + 'string' => 'Hello', + 'nullableString' => 'World', + ]); + + expect($data->toArray())->toEqual([ + 'string' => 'Hello', + 'nullableString' => 'World', + ]); }); it('can create a data object from FormRequest', function () { diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index d5a15a69..76b32c32 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -1296,6 +1296,46 @@ public static function allowedRequestIncludes(): ?array 'expectedPartials' => null, 'expectedResponse' => [], ]; + + yield 'with invalid partial definition' => [ + 'lazyDataAllowedIncludes' => null, + 'dataAllowedIncludes' => null, + 'includes' => '', + 'expectedPartials' => PartialsCollection::create(), + 'expectedResponse' => [], + ]; + + yield 'with non existing field' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => [], + 'includes' => 'non-existing', + 'expectedPartials' => PartialsCollection::create(), + 'expectedResponse' => [], + ]; + + yield 'with non existing nested field' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => [], + 'includes' => 'non-existing.still-non-existing', + 'expectedPartials' => PartialsCollection::create(), + 'expectedResponse' => [], + ]; + + yield 'with non allowed nested field' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => [], + 'includes' => 'nested.name', + 'expectedPartials' => PartialsCollection::create(), + 'expectedResponse' => [], + ]; + + yield 'with non allowed nested all' => [ + 'lazyDataAllowedIncludes' => [], + 'dataAllowedIncludes' => [], + 'includes' => 'nested.*', + 'expectedPartials' => PartialsCollection::create(), + 'expectedResponse' => [], + ]; }); it('can combine request and manual includes', function () { diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php index 3c539491..139ba8b1 100644 --- a/tests/Support/DataReturnTypeTest.php +++ b/tests/Support/DataReturnTypeTest.php @@ -10,6 +10,7 @@ use Spatie\LaravelData\Support\DataType; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; use Spatie\LaravelData\Support\Types\NamedType; +use Spatie\LaravelData\Support\Types\UnionType; use Spatie\LaravelData\Tests\Fakes\SimpleData; class TestReturnTypeSubject @@ -33,13 +34,23 @@ public function nullableArray(): ?array { } + + public function union(): array|Collection + { + + } + + public function none() + { + + } } it('can determine the return type from reflection', function ( string $methodName, string $typeName, mixed $value, - DataType $expected + ?DataType $expected ) { $factory = app(DataReturnTypeFactory::class); @@ -115,6 +126,43 @@ public function nullableArray(): ?array ]; }); +it('will return null when a method does not have a return type', function (){ + $factory = app(DataReturnTypeFactory::class); + + $reflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'none'); + + expect($factory->build($reflection, TestReturnTypeSubject::class))->toBeNull(); +}); + +it('can handle union types', function (){ + $factory = app(DataReturnTypeFactory::class); + + $reflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'union'); + + expect($factory->build($reflection, TestReturnTypeSubject::class))->toEqual( + new DataType( + type: new UnionType([ + new NamedType(Collection::class, false, [ + ArrayAccess::class, + CanBeEscapedWhenCastToString::class, + Enumerable::class, + Traversable::class, + Stringable::class, + JsonSerializable::class, + Jsonable::class, + IteratorAggregate::class, + Countable::class, + Arrayable::class, + ], DataTypeKind::DataEnumerable, null, null), + new NamedType('array', true, [], DataTypeKind::DataArray, null, null), + ]), + isNullable: false, + isMixed: false, + kind: DataTypeKind::DataEnumerable, // in the future this should be an array ... + ), + ); +}); + it('will store return types in the factory as a caching mechanism', function () { $factory = app(DataReturnTypeFactory::class); diff --git a/tests/Transformers/ArrayableTransformerTest.php b/tests/Transformers/ArrayableTransformerTest.php new file mode 100644 index 00000000..defed45c --- /dev/null +++ b/tests/Transformers/ArrayableTransformerTest.php @@ -0,0 +1,27 @@ +transform( + FakeDataStructureFactory::property($class, 'arrayable'), + $class->arrayable, + TransformationContextFactory::create()->get($class) + ) + )->toEqual(['A', 'B']); +}); diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 65077c32..2a692296 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Application; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator as ValidatorFacade; use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Exists as LaravelExists; @@ -712,6 +713,7 @@ class TestDataWithRootReferenceFieldValidationAttribute extends Data ->assertOk(['collection' => []]) ->assertErrors(['collection' => null]) ->assertErrors([]) + ->assertErrors(['collection' => ['strings', 'here', 'instead', 'of', 'arrays']]) ->assertErrors([ 'collection' => [ ['other_string' => 'Hello World'], @@ -1986,6 +1988,44 @@ public static function redirect(FakeInjectable $injectable): string ); }); +it('can manually set the redirect route', function () { + Route::get('/never-given-up', fn () => 'Never gonna give you up')->name('never-given-up'); + + $data = new class () extends Data { + public string $name; + + public static function redirectRoute(): string + { + return 'never-given-up'; + } + }; + + DataValidationAsserter::for($data)->assertRedirect( + payload: ['name' => null], + redirect: 'http://localhost/never-given-up' + ); +}); + +it('can resolve validation dependencies for redirect route', function () { + FakeInjectable::setup('Rick Astley'); + + Route::get('/never-given-up', fn () => 'Never gonna give you up')->name('never-given-up'); + + $data = new class () extends Data { + public string $name; + + public static function redirectRoute(FakeInjectable $injectable): string + { + return $injectable->value === 'Rick Astley' ? 'never-given-up' : 'given-up'; + } + }; + + DataValidationAsserter::for($data)->assertRedirect( + payload: ['name' => null], + redirect: 'http://localhost/never-given-up' + ); +}); + it('can manually specify the validator', function () { $dataClass = new class () extends Data { public string $property; From 3d65eb28e7b7adba9ed21ab28a402f35f360ec57 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 24 Jan 2024 10:20:35 +0000 Subject: [PATCH 112/124] Fix styling --- src/Concerns/BaseData.php | 1 - .../DataCollectableFromSomethingResolver.php | 1 - src/Resolvers/NameMappersResolver.php | 2 +- .../DataCollectableAnnotationReader.php | 4 ---- src/Support/Factories/DataClassFactory.php | 3 ++- tests/DataTest.php | 1 - .../CustomCursorPaginatedDataCollection.php | 1 - tests/MagicalCreationTest.php | 20 +++++++++---------- tests/MappingTest.php | 6 ++---- tests/Support/DataReturnTypeTest.php | 4 ++-- .../Transformers/ArrayableTransformerTest.php | 1 - 11 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 28ead482..06b12f28 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -9,7 +9,6 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\LazyCollection; -use Spatie\LaravelData\Contracts\BaseData as BaseDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 2d66231a..db0ac64b 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -21,7 +21,6 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; -use Spatie\LaravelData\Support\Types\NamedType; class DataCollectableFromSomethingResolver { diff --git a/src/Resolvers/NameMappersResolver.php b/src/Resolvers/NameMappersResolver.php index c3093d95..943726d1 100644 --- a/src/Resolvers/NameMappersResolver.php +++ b/src/Resolvers/NameMappersResolver.php @@ -76,7 +76,7 @@ protected function resolveMapperClass(int|string|NameMapper $value): NameMapper return new ProvidedNameMapper($value); } - if($value instanceof NameMapper){ + if($value instanceof NameMapper) { return $value; } diff --git a/src/Support/Annotations/DataCollectableAnnotationReader.php b/src/Support/Annotations/DataCollectableAnnotationReader.php index 54be1054..844afa6c 100644 --- a/src/Support/Annotations/DataCollectableAnnotationReader.php +++ b/src/Support/Annotations/DataCollectableAnnotationReader.php @@ -2,10 +2,7 @@ namespace Spatie\LaravelData\Support\Annotations; -use Illuminate\Contracts\Pagination\CursorPaginator; -use Illuminate\Pagination\AbstractPaginator; use Illuminate\Support\Arr; -use Illuminate\Support\Enumerable; use phpDocumentor\Reflection\FqsenResolver; use phpDocumentor\Reflection\Types\Context; use phpDocumentor\Reflection\Types\ContextFactory; @@ -13,7 +10,6 @@ use ReflectionMethod; use ReflectionProperty; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Contracts\BaseDataCollectable; /** * @note To myself, always use the fully qualified class names in pest tests when using anonymous classes diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php index 2247a219..17d0f67c 100644 --- a/src/Support/Factories/DataClassFactory.php +++ b/src/Support/Factories/DataClassFactory.php @@ -121,7 +121,8 @@ protected function resolveMethods( ): Collection { return collect($reflectionClass->getMethods()) ->filter(fn (ReflectionMethod $reflectionMethod) => str_starts_with($reflectionMethod->name, 'from') || str_starts_with($reflectionMethod->name, 'collect')) - ->reject(fn (ReflectionMethod $reflectionMethod) => in_array($reflectionMethod->name, ['from', 'collect', 'collection']) + ->reject( + fn (ReflectionMethod $reflectionMethod) => in_array($reflectionMethod->name, ['from', 'collect', 'collection']) || $reflectionMethod->isStatic() === false || $reflectionMethod->isPublic() === false ) diff --git a/tests/DataTest.php b/tests/DataTest.php index 3c98c93f..47edb71c 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -26,7 +26,6 @@ use Spatie\LaravelData\Tests\Fakes\SimpleDto; use Spatie\LaravelData\Tests\Fakes\SimpleResource; -use Symfony\Component\VarDumper\VarDumper; use function Spatie\Snapshots\assertMatchesSnapshot; it('also works by using traits and interfaces, skipping the base data class', function () { diff --git a/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php b/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php index 202232b4..a51f5725 100644 --- a/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php +++ b/tests/Fakes/DataCollections/CustomCursorPaginatedDataCollection.php @@ -3,7 +3,6 @@ namespace Spatie\LaravelData\Tests\Fakes\DataCollections; use Spatie\LaravelData\CursorPaginatedDataCollection; -use Spatie\LaravelData\PaginatedDataCollection; class CustomCursorPaginatedDataCollection extends CursorPaginatedDataCollection { diff --git a/tests/MagicalCreationTest.php b/tests/MagicalCreationTest.php index b53f9ba6..1bfb2d85 100644 --- a/tests/MagicalCreationTest.php +++ b/tests/MagicalCreationTest.php @@ -324,10 +324,10 @@ public static function collectArray(array $items, CreationContext $context): arr array|object $expected, ) { expect(SimpleData::collect(['A', 'B'], $into))->toEqual($expected); -})->with(function(){ +})->with(function () { yield 'array' => [ 'array', - fn() => [ + fn () => [ SimpleData::from('A'), SimpleData::from('B'), ], @@ -335,7 +335,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'laravel collection' => [ Collection::class, - fn() => collect([ + fn () => collect([ SimpleData::from('A'), SimpleData::from('B'), ]), @@ -343,7 +343,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'laravel lazy collection' => [ LazyCollection::class, - fn() =>new LazyCollection([ + fn () => new LazyCollection([ SimpleData::from('A'), SimpleData::from('B'), ]), @@ -351,7 +351,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'data collection' => [ DataCollection::class, - fn() => new DataCollection(SimpleData::class, [ + fn () => new DataCollection(SimpleData::class, [ SimpleData::from('A'), SimpleData::from('B'), ]), @@ -359,7 +359,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'data paginated collection' => [ PaginatedDataCollection::class, - fn() => new PaginatedDataCollection(SimpleData::class,new LengthAwarePaginator( [ + fn () => new PaginatedDataCollection(SimpleData::class, new LengthAwarePaginator([ SimpleData::from('A'), SimpleData::from('B'), ], 2, 15)), @@ -367,7 +367,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'data cursor paginated collection' => [ CursorPaginatedDataCollection::class, - fn() => new CursorPaginatedDataCollection(SimpleData::class,new CursorPaginator( [ + fn () => new CursorPaginatedDataCollection(SimpleData::class, new CursorPaginator([ SimpleData::from('A'), SimpleData::from('B'), ], 15)), @@ -375,7 +375,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'paginator' => [ LengthAwarePaginator::class, - fn() => new LengthAwarePaginator([ + fn () => new LengthAwarePaginator([ SimpleData::from('A'), SimpleData::from('B'), ], 2, 15), @@ -383,7 +383,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'cursor paginator' => [ CursorPaginator::class, - fn() => new CursorPaginator([ + fn () => new CursorPaginator([ SimpleData::from('A'), SimpleData::from('B'), ], 15), @@ -391,7 +391,7 @@ public static function collectArray(array $items, CreationContext $context): arr yield 'custom data collection' => [ CustomDataCollection::class, - fn() => new CustomDataCollection(SimpleData::class, [ + fn () => new CustomDataCollection(SimpleData::class, [ SimpleData::from('A'), SimpleData::from('B'), ]), diff --git a/tests/MappingTest.php b/tests/MappingTest.php index 6736b333..1840d963 100644 --- a/tests/MappingTest.php +++ b/tests/MappingTest.php @@ -317,9 +317,8 @@ public function __construct( ])); }); -it('has a mappers built in', function (){ - $data = new class extends Data - { +it('has a mappers built in', function () { + $data = new class () extends Data { #[MapName(CamelCaseMapper::class)] public string $camel_case = 'camelCase'; @@ -351,4 +350,3 @@ public function __construct( ->studly_case->toBe('StudlyCase') ->provided->toBe('provided'); }); - diff --git a/tests/Support/DataReturnTypeTest.php b/tests/Support/DataReturnTypeTest.php index 139ba8b1..955ab99c 100644 --- a/tests/Support/DataReturnTypeTest.php +++ b/tests/Support/DataReturnTypeTest.php @@ -126,7 +126,7 @@ public function none() ]; }); -it('will return null when a method does not have a return type', function (){ +it('will return null when a method does not have a return type', function () { $factory = app(DataReturnTypeFactory::class); $reflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'none'); @@ -134,7 +134,7 @@ public function none() expect($factory->build($reflection, TestReturnTypeSubject::class))->toBeNull(); }); -it('can handle union types', function (){ +it('can handle union types', function () { $factory = app(DataReturnTypeFactory::class); $reflection = new ReflectionMethod(\TestReturnTypeSubject::class, 'union'); diff --git a/tests/Transformers/ArrayableTransformerTest.php b/tests/Transformers/ArrayableTransformerTest.php index defed45c..3112efd3 100644 --- a/tests/Transformers/ArrayableTransformerTest.php +++ b/tests/Transformers/ArrayableTransformerTest.php @@ -4,7 +4,6 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; -use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Transformers\ArrayableTransformer; it('can transform an arrayable', function () { From d6c4804fc83e0e31b8e81c81f8cbdcdd56383896 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 24 Jan 2024 13:49:51 +0100 Subject: [PATCH 113/124] Use validation payload as data payload --- UPGRADING.md | 32 ++++++++++++++++++- src/DataPipes/ValidatePropertiesDataPipe.php | 4 +-- .../DataCollectableFromSomethingResolver.php | 19 +++++------ tests/ValidationTest.php | 16 ++++++---- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index 9294ebac..c06b9e73 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -119,6 +119,36 @@ Previously when trying to include, exclude, only or except certain data properti continued working. From now on an exception will be thrown, these exceptions can be silenced by setting `ignore_invalid_partials` to `true` within the config file +**Validated payloads (Likelihood Of Impact: Medium)** + +Previously when validating data, the data object was being created from the payload provided, not the validated payload. + +This payload can differ once you start using `exclude` rules which will exclude certain properties after validation. + +Be sure that these properties are now typed as `Optional` since they can be missing from the payload. + +```php +// v3 + +class SomeData extends Data { + #[ExcludeIf('excludeProperty', true)] + public string $property; + public bool $excludeProperty; +} +// Providing ['property' => 'something', 'excludeProperty' => true] will result in both fields set + +// v4 + +class SomeData extends Data { + #[ExcludeIf('excludeProperty', true)] + public string|Optional $property; + public bool $excludeProperty; +} +// Providing ['property' => 'something', 'excludeProperty' => true] will result in only the excludeProperty field set, the property field will be optional +``` + +Also notice, nested data objects will use the `required` rule, Laravel validation will not include the nested array in the validated payload when this array is empty. + **Internal data structure changes (Likelihood Of Impact: Low)** If you use internal data structures like `DataClass` and `DataProperty` then take a look at these classes, a lot as @@ -136,7 +166,7 @@ look what has changed: **ValidatePropertiesDataPipe (Likelihood Of Impact: Low)** -If you've used the `ValidatePropertiesDataPipe::allTypes` parameter to validate all types, then please use the new +If you've used the `ValidatePropertiesDataPipe::allTypes` parameter to validate all types, then please use Spatie\LaravelData\Attributes\Validation\ExcludeIf;use Spatie\LaravelData\Optional;use the new context when creating a data object to enable this or update your `data.php` config file with the new default. **Removal of `withoutMagicalCreationFrom` (Likelihood Of Impact: Low)** diff --git a/src/DataPipes/ValidatePropertiesDataPipe.php b/src/DataPipes/ValidatePropertiesDataPipe.php index 07ea8c10..94825db2 100644 --- a/src/DataPipes/ValidatePropertiesDataPipe.php +++ b/src/DataPipes/ValidatePropertiesDataPipe.php @@ -23,8 +23,6 @@ public function handle( return $properties; } - ($class->name)::validate($properties); - - return $properties; + return ($class->name)::validate($properties); } } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index db0ac64b..4e142832 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -37,29 +37,30 @@ public function execute( mixed $items, ?string $into = null, ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator { - $intoType = $into !== null - ? $this->dataReturnTypeFactory->buildFromNamedType($into, $dataClass, nullable: false) - : $this->dataReturnTypeFactory->buildFromValue($items, $dataClass, nullable: false); - $collectable = $this->createFromCustomCreationMethod($dataClass, $creationContext, $items, $into); if ($collectable) { return $collectable; } + /** @var NamedType $intoType */ + $intoType = $into !== null + ? $this->dataReturnTypeFactory->buildFromNamedType($into, $dataClass, nullable: false)->type + : $this->dataReturnTypeFactory->buildFromValue($items, $dataClass, nullable: false)->type; + $collectableMetaData = CollectableMetaData::fromOther($items); $normalizedItems = $this->normalizeItems($items, $dataClass, $creationContext); return match ($intoType->kind) { DataTypeKind::DataArray => $this->normalizeToArray($normalizedItems), - DataTypeKind::DataEnumerable => new $intoType->type->name($this->normalizeToArray($normalizedItems)), - DataTypeKind::DataCollection => new $intoType->type->name($dataClass, $this->normalizeToArray($normalizedItems)), - DataTypeKind::DataPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), - DataTypeKind::DataCursorPaginatedCollection => new $intoType->type->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::DataEnumerable => new $intoType->name($this->normalizeToArray($normalizedItems)), + DataTypeKind::DataCollection => new $intoType->name($dataClass, $this->normalizeToArray($normalizedItems)), + DataTypeKind::DataPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToPaginator($normalizedItems, $collectableMetaData)), + DataTypeKind::DataCursorPaginatedCollection => new $intoType->name($dataClass, $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData)), DataTypeKind::DataPaginator => $this->normalizeToPaginator($normalizedItems, $collectableMetaData), DataTypeKind::DataCursorPaginator => $this->normalizeToCursorPaginator($normalizedItems, $collectableMetaData), - default => throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->type->name) + default => throw CannotCreateDataCollectable::create(get_debug_type($items), $intoType->name) }; } diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 2a692296..451c0e9b 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -14,6 +14,10 @@ use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; +use Spatie\LaravelData\Attributes\Validation\ExcludeIf; +use Spatie\LaravelData\RuleInferrers\RuleInferrer; +use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Support\Validation\PropertyRules; use function Pest\Laravel\mock; use function PHPUnit\Framework\assertFalse; @@ -1052,7 +1056,7 @@ class CollectionClassC extends Data ], ] ); -})->skip(version_compare(Application::VERSION, '9.0', '<'), 'Laravel too old'); +}); it('supports required without validation for optional collections', function () { $dataClass = new class () extends Data { @@ -1154,7 +1158,7 @@ class CollectionClassK extends Data 'collection.0.nested.string' => [__('validation.email', ['attribute' => 'collection.0.nested.string'])], 'collection.2.nested.string' => [__('validation.email', ['attribute' => 'collection.2.nested.string'])], ]); -})->skip(version_compare(Application::VERSION, '9.0', '<'), 'Laravel too old'); +}); it('can nest data in deep collections using relative rule generation', function () { class ValidationTestDeepNestedDataWithContextOverwrittenRules extends Data @@ -1255,7 +1259,7 @@ public static function rules(ValidationContext $context): array 'collection.1.string' => [__('validation.email', ['attribute' => 'collection.1.string'])], 'collection.1.items.0.deep_string' => [__('validation.email', ['attribute' => 'collection.1.items.0.deep string'])], ]); -})->skip(version_compare(Application::VERSION, '9.0', '<'), 'Laravel too old'); +}); it('can nest data using relative rule generation', function () { $dataClass = new class () extends Data { @@ -1276,7 +1280,7 @@ public static function rules(ValidationContext $context): array 'nested.validate_as_email' => ['boolean', 'required'], ], $payload) ->assertErrors($payload); -})->skip(version_compare(Application::VERSION, '9.0', '<'), 'Laravel too old'); +}); it('correctly_injects_context_in_the_rules_method', function () { class NestedClassJ extends Data @@ -1436,7 +1440,7 @@ public static function rules(ValidationContext $context): array 'nested.collection.0.collection' => ['present', 'array'], 'nested.collection.0.collection.0.property' => ['required', 'string'], ], $payload); -})->skip(version_compare(Application::VERSION, '9.0', '<'), 'Laravel too old'); +}); it('will merge overwritten rules on inherited data objects', function () { $data = new class () extends Data { @@ -1459,7 +1463,7 @@ public static function rules(ValidationContext $context): array 'collection' => ['present', 'array'], 'collection.0.string' => ['string', 'required', 'min:10', 'max:100'], ], $payload)->assertErrors($payload); -})->skip(version_compare(Application::VERSION, '9.0', '<'), 'Laravel too old'); +}); it('will reduce attribute rules to Laravel rules in the end', function () { $dataClass = new class () extends Data { From 6a3aa4f055f840f475bba92c1acf1ef5854dd493 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 24 Jan 2024 12:50:46 +0000 Subject: [PATCH 114/124] Fix styling --- tests/ValidationTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 451c0e9b..2e8550e1 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -14,10 +14,6 @@ use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; -use Spatie\LaravelData\Attributes\Validation\ExcludeIf; -use Spatie\LaravelData\RuleInferrers\RuleInferrer; -use Spatie\LaravelData\Support\DataProperty; -use Spatie\LaravelData\Support\Validation\PropertyRules; use function Pest\Laravel\mock; use function PHPUnit\Framework\assertFalse; From 7c3c4f2a837ad266dd677e01b1004676379f5437 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 24 Jan 2024 15:05:17 +0100 Subject: [PATCH 115/124] Add property mapping context --- src/Concerns/BaseData.php | 8 +--- src/Concerns/ValidateableData.php | 4 +- src/Contracts/BaseData.php | 2 +- src/DataPipes/CastPropertiesDataPipe.php | 15 ++++--- src/DataPipes/MapPropertiesDataPipe.php | 41 ++++++++++++++++++- .../DataCollectableFromSomethingResolver.php | 25 ++++++++--- src/Support/Creation/CreationContext.php | 23 +++++++++++ .../Creation/CreationContextFactory.php | 2 + tests/ValidationTest.php | 20 +++++++++ 9 files changed, 116 insertions(+), 24 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 06b12f28..abb4508b 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -51,14 +51,8 @@ public static function collect(mixed $items, ?string $into = null): array|DataCo return static::factory()->collect($items, $into); } - public static function factory(?CreationContext $creationContext = null): CreationContextFactory|CreationContext + public static function factory(): CreationContextFactory { - if ($creationContext) { - $creationContext->dataClass = static::class; - - return $creationContext; - } - return CreationContextFactory::createFromConfig(static::class); } diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php index 12ff8be3..f148894e 100644 --- a/src/Concerns/ValidateableData.php +++ b/src/Concerns/ValidateableData.php @@ -49,9 +49,7 @@ public static function validate(Arrayable|array $payload): Arrayable|array public static function validateAndCreate(Arrayable|array $payload): static { - return static::factory() - ->alwaysValidate() - ->from($payload); + return static::factory()->alwaysValidate()->from($payload); } public static function withValidator(Validator $validator): void diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 2faf9ae8..0b6f73b4 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -44,7 +44,7 @@ public static function collect(mixed $items, ?string $into = null): array|DataCo * * @return ($creationContext is null ? CreationContextFactory : CreationContext) */ - public static function factory(?CreationContext $creationContext = null): CreationContextFactory|CreationContext; + public static function factory(): CreationContextFactory; public static function normalizers(): array; diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index 599f127c..d9e4bbc1 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -63,12 +63,15 @@ protected function cast( return $cast->cast($property, $value, $properties, $creationContext); } - if ($property->type->kind->isDataObject() && $property->type->dataClass) { - return $property->type->dataClass::factory($creationContext)->from($value); - } - - if ($property->type->kind->isDataCollectable()) { - return $property->type->dataClass::factory($creationContext)->collect($value, $property->type->dataCollectableClass); + if ( + $property->type->kind->isDataObject() + || $property->type->kind->isDataCollectable() + ) { + $context = $creationContext->next($property->type->dataClass, $property->name); + + return $property->type->kind->isDataObject() + ? $context->from($value) + : $context->collect($value, $property->type->dataCollectableClass); } return $value; diff --git a/src/DataPipes/MapPropertiesDataPipe.php b/src/DataPipes/MapPropertiesDataPipe.php index b8681df3..3c0c7b0b 100644 --- a/src/DataPipes/MapPropertiesDataPipe.php +++ b/src/DataPipes/MapPropertiesDataPipe.php @@ -5,6 +5,7 @@ use Illuminate\Support\Arr; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; +use Spatie\LaravelData\Support\DataProperty; class MapPropertiesDataPipe implements DataPipe { @@ -23,11 +24,47 @@ public function handle( continue; } - if (Arr::has($properties, $dataProperty->inputMappedName)) { - $properties[$dataProperty->name] = Arr::get($properties, $dataProperty->inputMappedName); + if (! Arr::has($properties, $dataProperty->inputMappedName)) { + continue; } + + $properties[$dataProperty->name] = Arr::get($properties, $dataProperty->inputMappedName); + + $this->addPropertyMappingToCreationContext( + $creationContext, + $dataProperty + ); } return $properties; } + + protected function addPropertyMappingToCreationContext( + CreationContext $creationContext, + DataProperty $property + ): void { + $depth = count($creationContext->currentPath); + + $mappedProperties = &$creationContext->mappedProperties; + + for ($i = 0; $i < $depth + 1; $i++) { + if ($i === $depth) { + if (! isset($mappedProperties['_mappings'])) { + $mappedProperties['_mappings'] = []; + } + + if ($property->type->kind->isDataCollectable() || $property->type->kind->isDataObject()) { + $mappedProperties['_mappings'][$property->name] = $property->inputMappedName; + } + + $mappedProperties['_mappings'][$property->name] = $property->inputMappedName; + } else { + if (! isset($mappedProperties[$creationContext->currentPath[$i]])) { + $mappedProperties[$creationContext->currentPath[$i]] = []; + } + + $mappedProperties = &$mappedProperties[$creationContext->currentPath[$i]]; + } + } + } } diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 4e142832..8b4fd40d 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -141,10 +141,13 @@ protected function normalizeItems( } if (is_array($items)) { - return array_map( - $this->itemsToDataClosure($dataClass, $creationContext), - $items - ); + $payload = []; + + foreach ($items as $index => $item) { + $payload[$index] = $this->itemsToDataClosure($dataClass, $creationContext)($item, $index); + } + + return $payload; } throw new Exception('Unable to normalize items'); @@ -200,6 +203,18 @@ protected function itemsToDataClosure( string $dataClass, CreationContext $creationContext ): Closure { - return fn (mixed $data) => $data instanceof $dataClass ? $data : $dataClass::factory($creationContext)->from($data); + return function (mixed $data, int|string $index) use ($dataClass, $creationContext) { + if ($data instanceof $dataClass) { + return $data; + } + + $creationContext->next($dataClass, $index); + + $data = $creationContext->from($data); + + $creationContext->previous(); + + return $data; + }; } } diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php index 917dce5a..3cb5e882 100644 --- a/src/Support/Creation/CreationContext.php +++ b/src/Support/Creation/CreationContext.php @@ -25,9 +25,12 @@ class CreationContext { /** * @param class-string $dataClass + * @param array $currentPath */ public function __construct( public string $dataClass, + public array $mappedProperties, + public array $currentPath, public readonly ValidationStrategy $validationStrategy, public readonly bool $mapPropertyNames, public readonly bool $disableMagicalCreation, @@ -67,4 +70,24 @@ public function collect( $into ); } + + /** @internal */ + public function next( + string $dataClass, + string|int $path, + ): self { + $this->dataClass = $dataClass; + + array_push($this->currentPath, $path); + + return $this; + } + + /** @internal */ + public function previous(): self + { + array_pop($this->currentPath); + + return $this; + } } diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 7fb01eef..3f791383 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -155,6 +155,8 @@ public function get(): CreationContext { return new CreationContext( dataClass: $this->dataClass, + mappedProperties: [], + currentPath: [], validationStrategy: $this->validationStrategy, mapPropertyNames: $this->mapPropertyNames, disableMagicalCreation: $this->disableMagicalCreation, diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 2e8550e1..36f869e5 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -2395,3 +2395,23 @@ public static function rules(ValidationContext $context): array expect(fn () => $dataClass::from(['string' => 'Nowp'])) ->toThrow(ValidationException::class); }); + +it('handles validation problem B', function (){ + #[MapInputName(SnakeCaseMapper::class)] + class CheerPointTeamRequest extends Data + { + public function __construct( + #[Required] + public readonly int $matchId, + + #[Required] + public readonly int $teamId, + ) { + } + } + + $data = CheerPointTeamRequest::factory()->alwaysValidate()->from([ + 'match_id' => 1, + 'team_id' => 2, + ]); +}); From 5933f401413a5bb84d28faf4742d0d5a68b93071 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Jan 2024 15:33:54 +0100 Subject: [PATCH 116/124] Fix small issues --- UPGRADING.md | 6 ++- docs/advanced-usage/mapping-rules.md | 18 ++++++++- src/Concerns/ValidateableData.php | 33 +++++------------ src/DataPipes/MapPropertiesDataPipe.php | 1 + src/DataPipes/ValidatePropertiesDataPipe.php | 10 ++++- src/Resolvers/DataValidationRulesResolver.php | 2 +- src/Resolvers/DataValidatorResolver.php | 6 ++- src/Resolvers/ValidatedPayloadResolver.php | 37 +++++++++++++++++++ src/Support/Creation/CreationContext.php | 2 +- src/Support/Creation/ValidationStrategy.php | 2 + src/Support/DataContainer.php | 17 +++++++++ src/Support/Validation/ValidationPath.php | 22 ++++++++--- .../RequiredRuleInferrerTest.php | 18 ++++----- .../Validation/RuleDenormalizerTest.php | 6 +-- tests/TestSupport/DataValidationAsserter.php | 2 +- tests/ValidationTest.php | 21 ++++++----- 16 files changed, 142 insertions(+), 61 deletions(-) create mode 100644 src/Resolvers/ValidatedPayloadResolver.php diff --git a/UPGRADING.md b/UPGRADING.md index c06b9e73..2ad2d6d1 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -73,7 +73,7 @@ public function cast(DataProperty $property, mixed $value, array $properties, Cr **The transform method (Likelihood Of Impact: Medium)** -The transform method singature was changed to use a factory pattern instead of paramaters: +The transform method signature was changed to use a factory pattern instead of parameters: ```php // v3 @@ -195,6 +195,10 @@ If you were using the `DataCollectableTransformer` or `DataTransformer` then ple If you were using the `DataObject` or `DataCollectable` interfaces then please replace the interfaces based upon the `Data` and `DataCollection` interfaces to your preference. +**ValidationPath changes (Likelihood Of Impact: Low)** + +If you were manually constructing a `ValidationPath` then please make sure to use an array instead of a string or null for the root level. + **Some advice with this new version of laravel-data** We advise you to take a look at the following things: diff --git a/docs/advanced-usage/mapping-rules.md b/docs/advanced-usage/mapping-rules.md index cac5297c..5ad389ce 100644 --- a/docs/advanced-usage/mapping-rules.md +++ b/docs/advanced-usage/mapping-rules.md @@ -23,9 +23,15 @@ class UserData extends Data public static function allowedRequestExcept(): ?array { return [ - 'song' // Use the original name when defining includes, excludes, excepts and only + 'song', // Use the original name when defining includes, excludes, excepts and only ]; } + + public function rules(ValidContext $context): array { + return [ + 'song' => 'required', // Use the original name when defining validation rules + ]; + } // ... } @@ -44,10 +50,18 @@ When adding an include, exclude, except or only: ```php UserData::from(User::first())->except('song'); // Always use the original name here - ``` +``` Within a request query, you can use the mapped or original name: ``` https://spatie.be/my-account?except[]=favorite_song ``` + +When validating a data object or getting rules for a data object, always use payloads use the original name: + +```php +UserData::validate($payload) +UserData::getValidationRules($payload) +``` + diff --git a/src/Concerns/ValidateableData.php b/src/Concerns/ValidateableData.php index f148894e..b48716e4 100644 --- a/src/Concerns/ValidateableData.php +++ b/src/Concerns/ValidateableData.php @@ -3,10 +3,9 @@ namespace Spatie\LaravelData\Concerns; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; use Spatie\LaravelData\Resolvers\DataValidationRulesResolver; -use Spatie\LaravelData\Resolvers\DataValidatorResolver; +use Spatie\LaravelData\Support\DataContainer; use Spatie\LaravelData\Support\Validation\DataRules; use Spatie\LaravelData\Support\Validation\ValidationContext; use Spatie\LaravelData\Support\Validation\ValidationPath; @@ -24,27 +23,15 @@ trait ValidateableData { public static function validate(Arrayable|array $payload): Arrayable|array { - $validator = app(DataValidatorResolver::class)->execute(static::class, $payload); - - try { - $validator->validate(); - } catch (ValidationException $exception) { - if (method_exists(static::class, 'redirect')) { - $exception->redirectTo(app()->call([static::class, 'redirect'])); - } - - if (method_exists(static::class, 'redirectRoute')) { - $exception->redirectTo(route(app()->call([static::class, 'redirectRoute']))); - } - - if (method_exists(static::class, 'errorBag')) { - $exception->errorBag(app()->call([static::class, 'errorBag'])); - } - - throw $exception; - } + $validator = DataContainer::get()->dataValidatorResolver()->execute( + static::class, + $payload, + ); - return $validator->validated(); + return DataContainer::get()->validatedPayloadResolver()->execute( + static::class, + $validator, + ); } public static function validateAndCreate(Arrayable|array $payload): static @@ -63,7 +50,7 @@ public static function getValidationRules(array $payload): array static::class, $payload, ValidationPath::create(), - DataRules::create() + DataRules::create(), ); } } diff --git a/src/DataPipes/MapPropertiesDataPipe.php b/src/DataPipes/MapPropertiesDataPipe.php index 3c0c7b0b..33ff6bb8 100644 --- a/src/DataPipes/MapPropertiesDataPipe.php +++ b/src/DataPipes/MapPropertiesDataPipe.php @@ -29,6 +29,7 @@ public function handle( } $properties[$dataProperty->name] = Arr::get($properties, $dataProperty->inputMappedName); +// Arr::forget($properties, $dataProperty->inputMappedName); $this->addPropertyMappingToCreationContext( $creationContext, diff --git a/src/DataPipes/ValidatePropertiesDataPipe.php b/src/DataPipes/ValidatePropertiesDataPipe.php index 94825db2..391e8d34 100644 --- a/src/DataPipes/ValidatePropertiesDataPipe.php +++ b/src/DataPipes/ValidatePropertiesDataPipe.php @@ -15,7 +15,9 @@ public function handle( array $properties, CreationContext $creationContext ): array { - if ($creationContext->validationStrategy === ValidationStrategy::Disabled) { + if ($creationContext->validationStrategy === ValidationStrategy::Disabled + || $creationContext->validationStrategy === ValidationStrategy::AlreadyRan + ) { return $properties; } @@ -23,6 +25,10 @@ public function handle( return $properties; } - return ($class->name)::validate($properties); + ($class->name)::validate($properties); + + $creationContext->validationStrategy = ValidationStrategy::AlreadyRan; + + return $properties; } } diff --git a/src/Resolvers/DataValidationRulesResolver.php b/src/Resolvers/DataValidationRulesResolver.php index 76dbf57b..1d52f55b 100644 --- a/src/Resolvers/DataValidationRulesResolver.php +++ b/src/Resolvers/DataValidationRulesResolver.php @@ -31,7 +31,7 @@ public function execute( string $class, array $fullPayload, ValidationPath $path, - DataRules $dataRules, + DataRules $dataRules ): array { $dataClass = $this->dataConfig->getDataClass($class); diff --git a/src/Resolvers/DataValidatorResolver.php b/src/Resolvers/DataValidatorResolver.php index b0fbf642..a5d073e3 100644 --- a/src/Resolvers/DataValidatorResolver.php +++ b/src/Resolvers/DataValidatorResolver.php @@ -19,8 +19,10 @@ public function __construct( } /** @param class-string $dataClass */ - public function execute(string $dataClass, Arrayable|array $payload): Validator - { + public function execute( + string $dataClass, + Arrayable|array $payload, + ): Validator { $payload = $payload instanceof Arrayable ? $payload->toArray() : $payload; $rules = $this->dataValidationRulesResolver->execute( diff --git a/src/Resolvers/ValidatedPayloadResolver.php b/src/Resolvers/ValidatedPayloadResolver.php new file mode 100644 index 00000000..6e2c162a --- /dev/null +++ b/src/Resolvers/ValidatedPayloadResolver.php @@ -0,0 +1,37 @@ + $dataClass */ + public function execute( + string $dataClass, + Validator $validator + ): array { + try { + $validator->validate(); + } catch (ValidationException $exception) { + if (method_exists($dataClass, 'redirect')) { + $exception->redirectTo(app()->call([$dataClass, 'redirect'])); + } + + if (method_exists($dataClass, 'redirectRoute')) { + $exception->redirectTo(route(app()->call([$dataClass, 'redirectRoute']))); + } + + if (method_exists($dataClass, 'errorBag')) { + $exception->errorBag(app()->call([$dataClass, 'errorBag'])); + } + + throw $exception; + } + + return $validator->validated(); + } +} diff --git a/src/Support/Creation/CreationContext.php b/src/Support/Creation/CreationContext.php index 3cb5e882..f7de0066 100644 --- a/src/Support/Creation/CreationContext.php +++ b/src/Support/Creation/CreationContext.php @@ -31,7 +31,7 @@ public function __construct( public string $dataClass, public array $mappedProperties, public array $currentPath, - public readonly ValidationStrategy $validationStrategy, + public ValidationStrategy $validationStrategy, public readonly bool $mapPropertyNames, public readonly bool $disableMagicalCreation, public readonly ?array $ignoredMagicalMethods, diff --git a/src/Support/Creation/ValidationStrategy.php b/src/Support/Creation/ValidationStrategy.php index a4de166d..2f6149b9 100644 --- a/src/Support/Creation/ValidationStrategy.php +++ b/src/Support/Creation/ValidationStrategy.php @@ -7,4 +7,6 @@ enum ValidationStrategy: string case Always = 'always'; case OnlyRequests = 'only_requests'; case Disabled = 'disabled'; + /** @internal */ + case AlreadyRan = 'already_ran'; } diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index 0644d03b..76dfc236 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -4,9 +4,12 @@ use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; +use Spatie\LaravelData\Resolvers\DataValidationRulesResolver; +use Spatie\LaravelData\Resolvers\DataValidatorResolver; use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectableResolver; use Spatie\LaravelData\Resolvers\TransformedDataResolver; +use Spatie\LaravelData\Resolvers\ValidatedPayloadResolver; use Spatie\LaravelData\Support\Factories\DataClassFactory; class DataContainer @@ -23,6 +26,10 @@ class DataContainer protected ?DataCollectableFromSomethingResolver $dataCollectableFromSomethingResolver = null; + protected ?DataValidatorResolver $dataValidatorResolver = null; + + protected ?ValidatedPayloadResolver $validatedPayloadResolver = null; + protected ?DataClassFactory $dataClassFactory = null; private function __construct() @@ -58,6 +65,16 @@ public function dataFromSomethingResolver(): DataFromSomethingResolver return $this->dataFromSomethingResolver ??= app(DataFromSomethingResolver::class); } + public function dataValidatorResolver(): DataValidatorResolver + { + return $this->dataValidatorResolver ??= app(DataValidatorResolver::class); + } + + public function validatedPayloadResolver(): ValidatedPayloadResolver + { + return $this->validatedPayloadResolver ??= app(ValidatedPayloadResolver::class); + } + public function dataCollectableFromSomethingResolver(): DataCollectableFromSomethingResolver { return $this->dataCollectableFromSomethingResolver ??= app(DataCollectableFromSomethingResolver::class); diff --git a/src/Support/Validation/ValidationPath.php b/src/Support/Validation/ValidationPath.php index 449860af..4344627e 100644 --- a/src/Support/Validation/ValidationPath.php +++ b/src/Support/Validation/ValidationPath.php @@ -7,35 +7,45 @@ class ValidationPath implements Stringable { public function __construct( - protected readonly ?string $path + protected readonly array $path = [] ) { } public static function create(?string $path = null): self { - return new self($path); + if ($path === null) { + return new self(); + } + + return new self(explode('.', $path)); } public function property(string $property): self { - return new self($this->path ? "{$this->path}.{$property}" : $property); + $newPath = $this->path; + + $newPath[] = $property; + + return new self($newPath); } public function isRoot(): bool { - return $this->path === null; + return empty($this->path); } public function equals(string|ValidationPath $other): bool { - $otherPath = $other instanceof ValidationPath ? $other->path : $other; + $otherPath = $other instanceof ValidationPath + ? $other->path + : explode('.', $other); return $this->path === $otherPath; } public function get(): ?string { - return $this->path; + return implode('.', $this->path); } public function __toString() diff --git a/tests/RuleInferrers/RequiredRuleInferrerTest.php b/tests/RuleInferrers/RequiredRuleInferrerTest.php index 0b2407aa..59b12b7f 100644 --- a/tests/RuleInferrers/RequiredRuleInferrerTest.php +++ b/tests/RuleInferrers/RequiredRuleInferrerTest.php @@ -38,7 +38,7 @@ function getProperty(object $class) public string $string; }); - $rules = $this->inferrer->handle($dataProperty, new PropertyRules(), new ValidationContext([], [], new ValidationPath(null))); + $rules = $this->inferrer->handle($dataProperty, new PropertyRules(), new ValidationContext([], [], ValidationPath::create(null))); expect($rules->all())->toEqualCanonicalizing([new Required()]); }); @@ -48,7 +48,7 @@ function getProperty(object $class) public ?string $string; }); - $rules = $this->inferrer->handle($dataProperty, new PropertyRules(), new ValidationContext([], [], new ValidationPath(null))); + $rules = $this->inferrer->handle($dataProperty, new PropertyRules(), new ValidationContext([], [], ValidationPath::create(null))); expect($rules->all())->toEqualCanonicalizing([]); }); @@ -61,7 +61,7 @@ function getProperty(object $class) $rules = $this->inferrer->handle( $dataProperty, PropertyRules::create()->add(new RequiredIf('bla')), - new ValidationContext([], [], new ValidationPath(null)) + new ValidationContext([], [], ValidationPath::create(null)) ); expect($rules->all())->toEqualCanonicalizing(['required_if:bla']); @@ -75,7 +75,7 @@ function getProperty(object $class) $rules = $this->inferrer->handle( $dataProperty, PropertyRules::create()->add(Required::create()), - new ValidationContext([], [], new ValidationPath(null)) + new ValidationContext([], [], ValidationPath::create(null)) ); expect(app(RuleDenormalizer::class)->execute($rules->all(), ValidationPath::create())) @@ -92,7 +92,7 @@ function () { $rules = $this->inferrer->handle( $dataProperty, PropertyRules::create()->add(BooleanType::create()), - new ValidationContext([], [], new ValidationPath(null)) + new ValidationContext([], [], ValidationPath::create(null)) ); expect(app(RuleDenormalizer::class)->execute($rules->all(), ValidationPath::create())) @@ -110,7 +110,7 @@ function () { $rules = $this->inferrer->handle( $dataProperty, PropertyRules::create()->add(Nullable::create()), - new ValidationContext([], [], new ValidationPath(null)) + new ValidationContext([], [], ValidationPath::create(null)) ); expect(app(RuleDenormalizer::class)->execute($rules->all(), ValidationPath::create())) @@ -128,7 +128,7 @@ function () { PropertyRules::create()->add( new \Spatie\LaravelData\Attributes\Validation\Enum(new BaseEnum('SomeClass')) ), - new ValidationContext([], [], new ValidationPath(null)) + new ValidationContext([], [], ValidationPath::create(null)) ); expect(app(RuleDenormalizer::class)->execute($rules->all(), ValidationPath::create()))->toEqualCanonicalizing([ @@ -145,7 +145,7 @@ function () { $rules = $this->inferrer->handle( $dataProperty, PropertyRules::create()->add(new Present(), new ArrayType()), - new ValidationContext([], [], new ValidationPath(null)) + new ValidationContext([], [], ValidationPath::create(null)) ); expect(app(RuleDenormalizer::class)->execute($rules->all(), ValidationPath::create())) @@ -157,7 +157,7 @@ function () { public string|Optional $string; }); - $rules = $this->inferrer->handle($dataProperty, [], new ValidationContext([], [], new ValidationPath(null))); + $rules = $this->inferrer->handle($dataProperty, [], new ValidationContext([], [], ValidationPath::create(null))); expect($rules)->toEqualCanonicalizing([]); })->throws(TypeError::class); diff --git a/tests/Support/Validation/RuleDenormalizerTest.php b/tests/Support/Validation/RuleDenormalizerTest.php index c961afb7..451285cb 100644 --- a/tests/Support/Validation/RuleDenormalizerTest.php +++ b/tests/Support/Validation/RuleDenormalizerTest.php @@ -26,7 +26,7 @@ it('can denormalize rules', function ($rule, $expected, $path = null) { $denormalizer = new RuleDenormalizer(); - expect($denormalizer->execute($rule, $path ?? new ValidationPath(null)))->toEqual($expected); + expect($denormalizer->execute($rule, $path ?? ValidationPath::create(null)))->toEqual($expected); })->with([ 'string rule' => ['string', ['string']], 'multi rule string' => ['string|required', ['string', 'required']], @@ -47,7 +47,7 @@ 'array parameter' => [new EndsWith(['test', DummyBackedEnum::BOO]), ['ends_with:test,boo']], 'date parameter' => [new After(CarbonImmutable::create(2020, 05, 16, 12, tz: new CarbonTimeZone('Europe/Brussels'))), ['after:2020-05-16T12:00:00+02:00']], 'field root reference parameter' => [new ExcludeWithout(new FieldReference('field')), ['exclude_without:field']], - 'field nested reference parameter' => [new ExcludeWithout(new FieldReference('field')), ['exclude_without:nested.field'], new ValidationPath('nested')], + 'field nested reference parameter' => [new ExcludeWithout(new FieldReference('field')), ['exclude_without:nested.field'], ValidationPath::create('nested')], ]); it('can denormalize rules with route parameter references', function () { @@ -57,7 +57,7 @@ $denormalizer = new RuleDenormalizer(); - expect($denormalizer->execute(new Min(new RouteParameterReference('parameter')), new ValidationPath(null)))->toEqual([ + expect($denormalizer->execute(new Min(new RouteParameterReference('parameter')), ValidationPath::create(null)))->toEqual([ 'min:69', ]); }); diff --git a/tests/TestSupport/DataValidationAsserter.php b/tests/TestSupport/DataValidationAsserter.php index ac33fa4f..dd3da335 100644 --- a/tests/TestSupport/DataValidationAsserter.php +++ b/tests/TestSupport/DataValidationAsserter.php @@ -85,7 +85,7 @@ public function assertRules( $this->dataClass, $this->pipePayload($payload), ValidationPath::create(), - DataRules::create() + DataRules::create(), ); $parser = new ValidationRuleParser($payload); diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 36f869e5..567ad753 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -2396,22 +2396,23 @@ public static function rules(ValidationContext $context): array ->toThrow(ValidationException::class); }); -it('handles validation problem B', function (){ +it('handles validation with mapped attributes', function (){ #[MapInputName(SnakeCaseMapper::class)] - class CheerPointTeamRequest extends Data + class TestValidationWithClassMappedAttribute extends Data { public function __construct( #[Required] - public readonly int $matchId, - - #[Required] - public readonly int $teamId, + public readonly int $someProperty, ) { } } - $data = CheerPointTeamRequest::factory()->alwaysValidate()->from([ - 'match_id' => 1, - 'team_id' => 2, + // Problem: + // some_property is mapped onto someProperty + // We generate rules for some_property -> we always generate rules for the mapped attribute if present + // So validation fails + + $data = TestValidationWithClassMappedAttribute::factory()->alwaysValidate()->from([ + 'some_property' => 1, ]); -}); +})->skip('Validation problem, fix in v5'); From a48b4cf836e5501895effcc54792e302d17e95fb Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Jan 2024 15:54:22 +0100 Subject: [PATCH 117/124] Fix PHPStan --- phpstan-baseline.neon | 9 --------- src/Contracts/BaseData.php | 10 ++++++---- src/Resolvers/DataCollectableFromSomethingResolver.php | 1 + src/Support/Creation/CreationContextFactory.php | 4 ++-- types/Factory.php | 3 --- 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 73827bcc..9f67caf9 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -75,21 +75,12 @@ parameters: count: 1 path: src/Data.php - - - message: "#^Method Spatie\\\\LaravelData\\\\Data\\:\\:validateAndCreate\\(\\) should return static\\(Spatie\\\\LaravelData\\\\Data\\) but returns Spatie\\\\LaravelData\\\\Contracts\\\\BaseData\\.$#" - count: 1 - path: src/Data.php - message: "#^PHPDoc tag @return with type Spatie\\\\LaravelData\\\\DataCollection\\ is not subtype of native type static\\(Spatie\\\\LaravelData\\\\DataCollection\\\\)\\.$#" count: 1 path: src/DataCollection.php - - - message: "#^Method Spatie\\\\LaravelData\\\\Dto\\:\\:validateAndCreate\\(\\) should return static\\(Spatie\\\\LaravelData\\\\Dto\\) but returns Spatie\\\\LaravelData\\\\Contracts\\\\BaseData\\.$#" - count: 1 - path: src/Dto.php - - message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Pagination\\\\Paginator\\:\\:count\\(\\)\\.$#" count: 1 diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 0b6f73b4..0511bf1a 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -20,11 +20,15 @@ use Spatie\LaravelData\Support\Creation\CreationContextFactory; /** - * @template TValue + * @template TData + * @template TValue of mixed * @template TKey of array-key */ interface BaseData { + /** + * @return static|null + */ public static function optional(mixed ...$payloads): ?static; /** @@ -40,9 +44,7 @@ public static function from(mixed ...$payloads): static; public static function collect(mixed $items, ?string $into = null): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection; /** - * @param CreationContext|null $creationContext - * - * @return ($creationContext is null ? CreationContextFactory : CreationContext) + * @return CreationContextFactory */ public static function factory(): CreationContextFactory; diff --git a/src/Resolvers/DataCollectableFromSomethingResolver.php b/src/Resolvers/DataCollectableFromSomethingResolver.php index 8b4fd40d..3841b127 100644 --- a/src/Resolvers/DataCollectableFromSomethingResolver.php +++ b/src/Resolvers/DataCollectableFromSomethingResolver.php @@ -21,6 +21,7 @@ use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; use Spatie\LaravelData\Support\Factories\DataReturnTypeFactory; +use Spatie\LaravelData\Support\Types\NamedType; class DataCollectableFromSomethingResolver { diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 3f791383..06d304b0 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -20,7 +20,7 @@ use Spatie\LaravelData\Support\DataContainer; /** - * @template TData of BaseData + * @template TData */ class CreationContextFactory { @@ -168,7 +168,7 @@ public function get(): CreationContext /** * @return TData */ - public function from(mixed ...$payloads): BaseData + public function from(mixed ...$payloads) { return DataContainer::get()->dataFromSomethingResolver()->execute( $this->dataClass, diff --git a/types/Factory.php b/types/Factory.php index b8c8beea..cc7e2c71 100644 --- a/types/Factory.php +++ b/types/Factory.php @@ -13,9 +13,6 @@ $factory = SimpleData::factory(); assertType(CreationContextFactory::class.'<'.SimpleData::class.'>', $factory); -$factory = SimpleDto::factory(CreationContextFactory::createFromConfig(SimpleData::class)->get()); -assertType(CreationContext::class.'<'.SimpleDto::class.'>' , $factory); // From SimpleData to SimpleDto - // Data $data = SimpleData::factory()->from('Hello World'); From 410c1792c6718b1c8dca9db1cec99f1899b73e19 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 25 Jan 2024 14:55:47 +0000 Subject: [PATCH 118/124] Fix styling --- src/Concerns/BaseData.php | 1 - src/Contracts/BaseData.php | 1 - src/DataPipes/MapPropertiesDataPipe.php | 2 +- src/Support/Creation/CreationContextFactory.php | 1 - src/Support/DataContainer.php | 1 - tests/ValidationTest.php | 2 +- 6 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index abb4508b..4141e66a 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -19,7 +19,6 @@ use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 0511bf1a..3f78bc9b 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -16,7 +16,6 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; use Spatie\LaravelData\PaginatedDataCollection; -use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; /** diff --git a/src/DataPipes/MapPropertiesDataPipe.php b/src/DataPipes/MapPropertiesDataPipe.php index 33ff6bb8..92d4e550 100644 --- a/src/DataPipes/MapPropertiesDataPipe.php +++ b/src/DataPipes/MapPropertiesDataPipe.php @@ -29,7 +29,7 @@ public function handle( } $properties[$dataProperty->name] = Arr::get($properties, $dataProperty->inputMappedName); -// Arr::forget($properties, $dataProperty->inputMappedName); + // Arr::forget($properties, $dataProperty->inputMappedName); $this->addPropertyMappingToCreationContext( $creationContext, diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 06d304b0..2bc1adab 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -13,7 +13,6 @@ use Illuminate\Support\Enumerable; use Illuminate\Support\LazyCollection; use Spatie\LaravelData\Casts\Cast; -use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; diff --git a/src/Support/DataContainer.php b/src/Support/DataContainer.php index 76dfc236..eeb4a511 100644 --- a/src/Support/DataContainer.php +++ b/src/Support/DataContainer.php @@ -4,7 +4,6 @@ use Spatie\LaravelData\Resolvers\DataCollectableFromSomethingResolver; use Spatie\LaravelData\Resolvers\DataFromSomethingResolver; -use Spatie\LaravelData\Resolvers\DataValidationRulesResolver; use Spatie\LaravelData\Resolvers\DataValidatorResolver; use Spatie\LaravelData\Resolvers\RequestQueryStringPartialsResolver; use Spatie\LaravelData\Resolvers\TransformedDataCollectableResolver; diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 567ad753..4bfe3b53 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -2396,7 +2396,7 @@ public static function rules(ValidationContext $context): array ->toThrow(ValidationException::class); }); -it('handles validation with mapped attributes', function (){ +it('handles validation with mapped attributes', function () { #[MapInputName(SnakeCaseMapper::class)] class TestValidationWithClassMappedAttribute extends Data { From 19e5560ec7c946ae51358edfc4397e3adeb5dd23 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Jan 2024 16:17:19 +0100 Subject: [PATCH 119/124] Add extra serialization casts and transformerts --- src/Casts/UnserializeCast.php | 33 ++++++++++ src/Transformers/SerializeTransformer.php | 15 +++++ tests/Casts/UnserializeCastTest.php | 61 +++++++++++++++++++ .../Transformers/SerializeTransformerTest.php | 27 ++++++++ 4 files changed, 136 insertions(+) create mode 100644 src/Casts/UnserializeCast.php create mode 100644 src/Transformers/SerializeTransformer.php create mode 100644 tests/Casts/UnserializeCastTest.php create mode 100644 tests/Transformers/SerializeTransformerTest.php diff --git a/src/Casts/UnserializeCast.php b/src/Casts/UnserializeCast.php new file mode 100644 index 00000000..867d72b7 --- /dev/null +++ b/src/Casts/UnserializeCast.php @@ -0,0 +1,33 @@ +failSilently){ + return Uncastable::create(); + } + + throw $e; + } + } +} diff --git a/src/Transformers/SerializeTransformer.php b/src/Transformers/SerializeTransformer.php new file mode 100644 index 00000000..dd0f7464 --- /dev/null +++ b/src/Transformers/SerializeTransformer.php @@ -0,0 +1,15 @@ +cast( + FakeDataStructureFactory::property($class, 'enum'), + $value, + [], + CreationContextFactory::createFromConfig($class::class)->get() + ) + )->toEqual(DummyBackedEnum::FOO); +}); + +it('will throw an exception when the unserialization fails', function (){ + $class = new class () { + public DummyBackedEnum $enum; + }; + + $cast = new UnserializeCast(); + + expect( + fn() => $cast->cast( + FakeDataStructureFactory::property($class, 'enum'), + 'foo', + [], + CreationContextFactory::createFromConfig($class::class)->get() + ) + )->toThrow(ErrorException::class); +}); + +it('can fail silently', function (){ + $class = new class () { + public DummyBackedEnum $enum; + }; + + $cast = new UnserializeCast(true); + + expect( + $cast->cast( + FakeDataStructureFactory::property($class, 'enum'), + 'foo', + [], + CreationContextFactory::createFromConfig($class::class)->get() + ) + )->toBeInstanceOf(Uncastable::class); +}); diff --git a/tests/Transformers/SerializeTransformerTest.php b/tests/Transformers/SerializeTransformerTest.php new file mode 100644 index 00000000..ad8e54fc --- /dev/null +++ b/tests/Transformers/SerializeTransformerTest.php @@ -0,0 +1,27 @@ +transform( + FakeDataStructureFactory::property($class, 'enum'), + $class->enum, + TransformationContextFactory::create()->get($class) + ) + )->toEqual(serialize(DummyBackedEnum::FOO)); +}); From 5e9d5db083174c3b4785e621ecfe132ea40e1875 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 25 Jan 2024 15:17:45 +0000 Subject: [PATCH 120/124] Fix styling --- src/Casts/UnserializeCast.php | 5 ++--- src/Transformers/SerializeTransformer.php | 1 - tests/Casts/UnserializeCastTest.php | 7 +++---- tests/Transformers/SerializeTransformerTest.php | 2 -- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Casts/UnserializeCast.php b/src/Casts/UnserializeCast.php index 867d72b7..d1716901 100644 --- a/src/Casts/UnserializeCast.php +++ b/src/Casts/UnserializeCast.php @@ -9,8 +9,7 @@ class UnserializeCast implements Cast { public function __construct( private bool $failSilently = false, - ) - { + ) { } public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed @@ -23,7 +22,7 @@ public function cast(DataProperty $property, mixed $value, array $properties, Cr try { return unserialize($value); } catch (\Throwable $e) { - if($this->failSilently){ + if($this->failSilently) { return Uncastable::create(); } diff --git a/src/Transformers/SerializeTransformer.php b/src/Transformers/SerializeTransformer.php index dd0f7464..67f45492 100644 --- a/src/Transformers/SerializeTransformer.php +++ b/src/Transformers/SerializeTransformer.php @@ -2,7 +2,6 @@ namespace Spatie\LaravelData\Transformers; -use App\Support\Models\Model; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContext; diff --git a/tests/Casts/UnserializeCastTest.php b/tests/Casts/UnserializeCastTest.php index 6b9bccb6..9b43c0de 100644 --- a/tests/Casts/UnserializeCastTest.php +++ b/tests/Casts/UnserializeCastTest.php @@ -1,6 +1,5 @@ toEqual(DummyBackedEnum::FOO); }); -it('will throw an exception when the unserialization fails', function (){ +it('will throw an exception when the unserialization fails', function () { $class = new class () { public DummyBackedEnum $enum; }; @@ -34,7 +33,7 @@ $cast = new UnserializeCast(); expect( - fn() => $cast->cast( + fn () => $cast->cast( FakeDataStructureFactory::property($class, 'enum'), 'foo', [], @@ -43,7 +42,7 @@ )->toThrow(ErrorException::class); }); -it('can fail silently', function (){ +it('can fail silently', function () { $class = new class () { public DummyBackedEnum $enum; }; diff --git a/tests/Transformers/SerializeTransformerTest.php b/tests/Transformers/SerializeTransformerTest.php index ad8e54fc..41bf36c9 100644 --- a/tests/Transformers/SerializeTransformerTest.php +++ b/tests/Transformers/SerializeTransformerTest.php @@ -6,9 +6,7 @@ use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; use Spatie\LaravelData\Tests\Factories\FakeDataStructureFactory; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; -use Spatie\LaravelData\Transformers\EnumTransformer; use Spatie\LaravelData\Transformers\SerializeTransformer; -use Tests\TestCase; it('can transform using a serializer', function () { $transformer = new SerializeTransformer(); From bfb347929538b976c83ac1d9c59f73ebee0b0818 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Jan 2024 16:38:54 +0100 Subject: [PATCH 121/124] Add support for passing in creation contexts within the factory --- src/Concerns/BaseData.php | 7 ++++++- src/Contracts/BaseData.php | 3 ++- src/Support/Creation/CreationContextFactory.php | 17 +++++++++++++++-- tests/CreationFactoryTest.php | 13 +++++++++++++ 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 4141e66a..3c8ae595 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -19,6 +19,7 @@ use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataProperty; @@ -50,8 +51,12 @@ public static function collect(mixed $items, ?string $into = null): array|DataCo return static::factory()->collect($items, $into); } - public static function factory(): CreationContextFactory + public static function factory(?CreationContext $creationContext = null): CreationContextFactory { + if ($creationContext) { + return CreationContextFactory::createFromCreationContext($creationContext); + } + return CreationContextFactory::createFromConfig(static::class); } diff --git a/src/Contracts/BaseData.php b/src/Contracts/BaseData.php index 3f78bc9b..fdb30c74 100644 --- a/src/Contracts/BaseData.php +++ b/src/Contracts/BaseData.php @@ -16,6 +16,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\DataPipeline; use Spatie\LaravelData\PaginatedDataCollection; +use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\Creation\CreationContextFactory; /** @@ -45,7 +46,7 @@ public static function collect(mixed $items, ?string $into = null): array|DataCo /** * @return CreationContextFactory */ - public static function factory(): CreationContextFactory; + public static function factory(?CreationContext $creationContext = null): CreationContextFactory; public static function normalizers(): array; diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 2bc1adab..3406b501 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -52,6 +52,19 @@ public static function createFromConfig( ); } + public static function createFromCreationContext( + CreationContext $creationContext + ): self { + return new self( + dataClass: $creationContext->dataClass, + validationStrategy: $creationContext->validationStrategy, + mapPropertyNames: $creationContext->mapPropertyNames, + disableMagicalCreation: $creationContext->disableMagicalCreation, + ignoredMagicalMethods: $creationContext->ignoredMagicalMethods, + casts: $creationContext->casts, + ); + } + public function validationStrategy(ValidationStrategy $validationStrategy): self { $this->validationStrategy = $validationStrategy; @@ -123,7 +136,7 @@ public function ignoreMagicalMethod(string ...$methods): self */ public function withCast( string $castable, - Cast | string $cast, + Cast|string $cast, ): self { $cast = is_string($cast) ? app($cast) : $cast; @@ -187,7 +200,7 @@ public function from(mixed ...$payloads) public function collect( mixed $items, ?string $into = null - ): array | DataCollection | PaginatedDataCollection | CursorPaginatedDataCollection | Enumerable | AbstractPaginator | PaginatorContract | AbstractCursorPaginator | CursorPaginatorContract | LazyCollection | Collection { + ): array|DataCollection|PaginatedDataCollection|CursorPaginatedDataCollection|Enumerable|AbstractPaginator|PaginatorContract|AbstractCursorPaginator|CursorPaginatorContract|LazyCollection|Collection { return DataContainer::get()->dataCollectableFromSomethingResolver()->execute( $this->dataClass, $this->get(), diff --git a/tests/CreationFactoryTest.php b/tests/CreationFactoryTest.php index c512024c..ad9d16f9 100644 --- a/tests/CreationFactoryTest.php +++ b/tests/CreationFactoryTest.php @@ -5,7 +5,9 @@ use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\Validation\In; use Spatie\LaravelData\Data; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\Creation\GlobalCastsCollection; +use Spatie\LaravelData\Support\Creation\ValidationStrategy; use Spatie\LaravelData\Tests\Fakes\Casts\MeaningOfLifeCast; use Spatie\LaravelData\Tests\Fakes\Casts\StringToUpperCast; use Spatie\LaravelData\Tests\Fakes\SimpleData; @@ -148,3 +150,14 @@ public static function fromArray(array $payload) ->first()->string->toEqual('HELLO WORLD') ->last()->string->toEqual('HELLO YOU'); }); + + +it('is possible to pass another creationContext to a factory as base', function (){ + $baseCreationContext = CreationContextFactory::createFromConfig(SimpleData::class) + ->alwaysValidate() + ->get(); + + $creationContext = SimpleData::factory($baseCreationContext)->get(); + + expect($creationContext->validationStrategy)->toBe(ValidationStrategy::Always); +}); From 3c3ffcdac54f094b9b3cac2096b8efd96473da7d Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Thu, 25 Jan 2024 15:39:26 +0000 Subject: [PATCH 122/124] Fix styling --- tests/CreationFactoryTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CreationFactoryTest.php b/tests/CreationFactoryTest.php index ad9d16f9..09175691 100644 --- a/tests/CreationFactoryTest.php +++ b/tests/CreationFactoryTest.php @@ -152,7 +152,7 @@ public static function fromArray(array $payload) }); -it('is possible to pass another creationContext to a factory as base', function (){ +it('is possible to pass another creationContext to a factory as base', function () { $baseCreationContext = CreationContextFactory::createFromConfig(SimpleData::class) ->alwaysValidate() ->get(); From 23ea3335fce8adb61fc0a2380b4848f680508ab6 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Thu, 25 Jan 2024 16:52:16 +0100 Subject: [PATCH 123/124] wip --- src/Concerns/BaseData.php | 2 +- src/Support/Creation/CreationContextFactory.php | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index 3c8ae595..beac3bdf 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -54,7 +54,7 @@ public static function collect(mixed $items, ?string $into = null): array|DataCo public static function factory(?CreationContext $creationContext = null): CreationContextFactory { if ($creationContext) { - return CreationContextFactory::createFromCreationContext($creationContext); + return CreationContextFactory::createFromCreationContext(static::class, $creationContext); } return CreationContextFactory::createFromConfig(static::class); diff --git a/src/Support/Creation/CreationContextFactory.php b/src/Support/Creation/CreationContextFactory.php index 3406b501..fed7ff32 100644 --- a/src/Support/Creation/CreationContextFactory.php +++ b/src/Support/Creation/CreationContextFactory.php @@ -53,10 +53,11 @@ public static function createFromConfig( } public static function createFromCreationContext( - CreationContext $creationContext + string $dataClass, + CreationContext $creationContext, ): self { return new self( - dataClass: $creationContext->dataClass, + dataClass: $dataClass, validationStrategy: $creationContext->validationStrategy, mapPropertyNames: $creationContext->mapPropertyNames, disableMagicalCreation: $creationContext->disableMagicalCreation, From b8ea8abc8a99ddb262c92699c6237dda2a882f51 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 29 Jan 2024 10:34:28 +0100 Subject: [PATCH 124/124] wip --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2465f4e..b85cb3f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,12 @@ All notable changes to `laravel-data` will be documented in this file. - Add support for using Laravel Model attributes as data properties - Allow creating data objects using `from` without parameters - Add support for a Dto and Resource object +- It is now a lot easier to validate all the payloads added to laravel-data - Added contexts to the creation and transformation process - Allow creating a data object or collection using a factory - Speed up the process of creating and transforming data objects - Add support for BNF syntax +- Laravel 10 requirement - Rewritten docs **Some more "internal" changes**