From 1e303244728347952dd7bd5e3e9841b6d9fc0c8f Mon Sep 17 00:00:00 2001 From: edalzell Date: Fri, 13 Oct 2023 15:40:56 -0700 Subject: [PATCH 1/7] Add DataCollectionSynth --- src/Synths/DataCollectionSynth.php | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/Synths/DataCollectionSynth.php diff --git a/src/Synths/DataCollectionSynth.php b/src/Synths/DataCollectionSynth.php new file mode 100644 index 00000000..2adfa80b --- /dev/null +++ b/src/Synths/DataCollectionSynth.php @@ -0,0 +1,36 @@ +toArray(), ['class' => $target->dataClass]]; + } + + public function hydrate($value, $meta) + { + return new DataCollection($meta['class'], $value); + } + + public function get(&$target, $key) + { + return $target[$key] ?? null; + } + + public function set(&$target, $key, $value) + { + $target[$key] = $value; + } +} From 173a546363dda4446096d782e23e386b447d649c Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 13 Dec 2023 15:15:33 -0800 Subject: [PATCH 2/7] deydrate recursively --- src/Synths/DataCollectionSynth.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Synths/DataCollectionSynth.php b/src/Synths/DataCollectionSynth.php index 2adfa80b..50a1a69a 100644 --- a/src/Synths/DataCollectionSynth.php +++ b/src/Synths/DataCollectionSynth.php @@ -14,9 +14,18 @@ public static function match($target) return get_class($target) == DataCollection::class; } - public function dehydrate(DataCollection $target) + public function dehydrate(DataCollection $target, $dehydrateChild) { - return [$target->toArray(), ['class' => $target->dataClass]]; + $data = $target->all(); + + foreach ($data as $key => $child) { + $data[$key] = $dehydrateChild($key, $child); + } + + return [ + $data, + ['class' => get_class($target)], + ]; } public function hydrate($value, $meta) From d0296a207db89a4976208a3a6e74b84da8485356 Mon Sep 17 00:00:00 2001 From: edalzell Date: Wed, 13 Dec 2023 16:55:27 -0800 Subject: [PATCH 3/7] same with hydrate --- src/Synths/DataCollectionSynth.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Synths/DataCollectionSynth.php b/src/Synths/DataCollectionSynth.php index 50a1a69a..c4591eb5 100644 --- a/src/Synths/DataCollectionSynth.php +++ b/src/Synths/DataCollectionSynth.php @@ -28,9 +28,19 @@ public function dehydrate(DataCollection $target, $dehydrateChild) ]; } - public function hydrate($value, $meta) + /** + * @param array $value + * @param array $meta + * @param mixed $hydrateChild + * @return \Spatie\LaravelData\DataCollection + */ + public function hydrate($value, $meta, $hydrateChild) { - return new DataCollection($meta['class'], $value); + foreach ($value as $key => $child) { + $value[$key] = $hydrateChild($key, $child); + } + + return $meta['class']::make($value); } public function get(&$target, $key) From a3d6f75ab1b9e200486334ef0960cebabce93b13 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Mon, 19 Feb 2024 12:50:19 +0100 Subject: [PATCH 4/7] First changes so that Laravel Data and livewire work better together --- composer.json | 1 + src/LaravelDataServiceProvider.php | 11 ++ src/Support/Lazy/LivewireLostLazy.php | 20 ++++ src/Support/Livewire/LivewireDataSynth.php | 119 +++++++++++++++++++++ 4 files changed, 151 insertions(+) create mode 100644 src/Support/Lazy/LivewireLostLazy.php create mode 100644 src/Support/Livewire/LivewireDataSynth.php diff --git a/composer.json b/composer.json index ac0622a9..65f4066b 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "spatie/php-structure-discoverer": "^2.0" }, "require-dev" : { + "livewire/livewire" : "^3.0", "fakerphp/faker": "^1.14", "friendsofphp/php-cs-fixer": "^3.0", "inertiajs/inertia-laravel": "dev-master#4508fd1", diff --git a/src/LaravelDataServiceProvider.php b/src/LaravelDataServiceProvider.php index f748b0ee..d9b648af 100644 --- a/src/LaravelDataServiceProvider.php +++ b/src/LaravelDataServiceProvider.php @@ -2,11 +2,13 @@ namespace Spatie\LaravelData; +use Livewire\Livewire; use Spatie\LaravelData\Commands\DataMakeCommand; use Spatie\LaravelData\Commands\DataStructuresCacheCommand; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Support\Caching\DataStructureCache; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\Livewire\LivewireDataSynth; use Spatie\LaravelData\Support\VarDumper\VarDumperManager; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -50,6 +52,15 @@ function () { fn ($container) => $class::from($container['request']) ); }); + + if(class_exists(Livewire::class)) { + $this->registerLivewireSynths(); + } + } + + protected function registerLivewireSynths(): void + { + Livewire::propertySynthesizer(LivewireDataSynth::class); } public function packageBooted(): void diff --git a/src/Support/Lazy/LivewireLostLazy.php b/src/Support/Lazy/LivewireLostLazy.php new file mode 100644 index 00000000..2e945241 --- /dev/null +++ b/src/Support/Lazy/LivewireLostLazy.php @@ -0,0 +1,20 @@ +dataClass}::{$this->propertyName}` was lost when the data object was transformed to be used by Livewire. You can include the property and then the correct value will be set when creating the data object from Livewire again."); + } +} diff --git a/src/Support/Livewire/LivewireDataSynth.php b/src/Support/Livewire/LivewireDataSynth.php new file mode 100644 index 00000000..78b306dd --- /dev/null +++ b/src/Support/Livewire/LivewireDataSynth.php @@ -0,0 +1,119 @@ + probably a better default + // What if we want to create a new data object and don't have the lazyvalue + // -> we could create a new LiveWireMissingLazy object, which we then use as the lazy value + // -> when resolving it would throw an error saying the value was lost in LiveWire + // + // Problem with computed properties should be sorted out + // + // DataCollections synth from the PR + // + // Do we want livewire as an dependency? + // + // Update docs + // + // Can we test this? + // + // Mapping property names, should we do this? + + + protected DataConfig $dataConfig; + + public static string $key = 'laravel-data-object'; + + public function __construct(ComponentContext $context, $path) + { + $this->dataConfig = app(DataConfig::class); + + parent::__construct($context, $path); + } + + public static function match($target) + { + return $target instanceof BaseData && $target instanceof TransformableData; + } + + public function get(&$target, $key): BaseData + { + return $target->{$key}; + } + + public function set(&$target, $key, $value): void + { + $target->{$key} = $value; + } + + /** + * @param BaseData&TransformableData $target + * @param callable(mixed):mixed $dehydrateChild + * + * @return array + */ + public function dehydrate( + BaseData&TransformableData $target, + callable $dehydrateChild + ): array { + $morph = $this->dataConfig->morphMap->getDataClassAlias($target::class) ?? $target::class; + + $payload = $target->all(); + + ray($payload); + + foreach ($payload as $key => $value) { + $payload[$key] = $dehydrateChild($key, $value); + } + + return [ + $payload, + ['morph' => $morph], + ]; + } + + /** + * @param mixed $value + * @param array $meta + * @param callable(mixed):mixed $hydrateChild + * + * @return BaseData + */ + public function hydrate( + array $value, + array $meta, + callable $hydrateChild + ): BaseData { + $morph = $meta['morph']; + + $dataClass = $this->dataConfig->morphMap->getMorphedDataClass($morph) ?? $morph; + + $payload = []; + + foreach ($this->dataConfig->getDataClass($dataClass)->properties as $name => $property) { + if (array_key_exists($name, $value) === false && $property->type->lazyType) { + $payload[$name] = new LivewireLostLazy($dataClass, $name); + + continue; + } + + $payload[$name] = $hydrateChild($name, $value[$name]); + } + + return $dataClass::factory() + ->ignoreMagicalMethod('fromLivewire') + ->from($payload); + } +} From 9062c2a440ea47252408b045f6446cdd88f46bc4 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Mar 2024 13:07:04 +0100 Subject: [PATCH 5/7] Livewire updates --- composer.json | 4 +- config/data.php | 16 +++- docs/advanced-usage/use-with-livewire.md | 83 ++++++++++++++++++- src/Concerns/ContextableData.php | 8 ++ src/LaravelDataServiceProvider.php | 2 +- src/Support/DataClassMorphMap.php | 3 + src/Support/Livewire/LivewireDataSynth.php | 56 ++++++------- src/Support/Partials/Partial.php | 25 ++++++ src/Support/Partials/PartialsCollection.php | 21 +++++ src/Support/Transformation/DataContext.php | 32 +++++++ src/Support/Wrapping/Wrap.php | 16 ++++ src/Support/Wrapping/WrapType.php | 8 +- tests/Fakes/ComputedData.php | 17 ++++ .../Fakes/Livewire/ComputedDataComponent.php | 33 ++++++++ tests/Fakes/Livewire/MappedDataComponent.php | 32 +++++++ tests/Fakes/Livewire/NestedDataComponent.php | 36 ++++++++ tests/Fakes/Livewire/SimpleDataComponent.php | 35 ++++++++ tests/LivewireTest.php | 75 +++++++++++++++++ .../Transformation/DataContextTest.php | 54 ++++++++++++ tests/TestCase.php | 2 + 20 files changed, 516 insertions(+), 42 deletions(-) create mode 100644 tests/Fakes/ComputedData.php create mode 100644 tests/Fakes/Livewire/ComputedDataComponent.php create mode 100644 tests/Fakes/Livewire/MappedDataComponent.php create mode 100644 tests/Fakes/Livewire/NestedDataComponent.php create mode 100644 tests/Fakes/Livewire/SimpleDataComponent.php create mode 100644 tests/Support/Transformation/DataContextTest.php diff --git a/composer.json b/composer.json index 65f4066b..26c7becc 100644 --- a/composer.json +++ b/composer.json @@ -23,17 +23,17 @@ "spatie/php-structure-discoverer": "^2.0" }, "require-dev" : { - "livewire/livewire" : "^3.0", "fakerphp/faker": "^1.14", "friendsofphp/php-cs-fixer": "^3.0", "inertiajs/inertia-laravel": "dev-master#4508fd1", + "livewire/livewire": "^3.0", "mockery/mockery": "^1.6", "nesbot/carbon": "^2.63", - "nette/php-generator": "^3.5", "nunomaduro/larastan": "^2.0", "orchestra/testbench": "^8.0|^9.0", "pestphp/pest": "^2.31", "pestphp/pest-plugin-laravel": "^2.0", + "pestphp/pest-plugin-livewire": "^2.1", "phpbench/phpbench": "^1.2", "phpstan/extension-installer": "^1.1", "phpunit/phpunit": "^10.0", diff --git a/config/data.php b/config/data.php index 81fae442..b500f10d 100644 --- a/config/data.php +++ b/config/data.php @@ -1,6 +1,5 @@ [ - /* + /** * Provides default configuration for the `make:data` command. These settings can be overridden with options * passed directly to the `make:data` command for generating single Data classes, or if not set they will * automatically fall back to these defaults. See `php artisan make:data --help` for more information */ 'make' => [ - /* + /** * The default namespace for generated Data classes. This exists under the application's root namespace, * so the default 'Data` will end up as '\App\Data', and generated Data classes will be placed in the * app/Data/ folder. Data classes can live anywhere, but this is where `make:data` will put them. */ 'namespace' => 'Data', - /* + /** * This suffix will be appended to all data classes generated by make:data, so that they are less likely * to conflict with other related classes, controllers or models with a similar name without resorting * to adding an alias for the Data object. Set to a blank string (not null) to disable. @@ -132,4 +131,13 @@ 'suffix' => 'Data', ], ], + + /** + * When using Livewire, the package allows you to enable or disable the synths + * these synths will automatically handle the data objects and their + * properties when used in a Livewire component. + */ + 'livewire' => [ + 'enable_synths' => false, + ], ]; diff --git a/docs/advanced-usage/use-with-livewire.md b/docs/advanced-usage/use-with-livewire.md index 57d528a3..0c43696a 100644 --- a/docs/advanced-usage/use-with-livewire.md +++ b/docs/advanced-usage/use-with-livewire.md @@ -3,7 +3,8 @@ title: Use with Livewire weight: 10 --- -> Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple without leaving the comfort of Laravel. +> Livewire is a full-stack framework for Laravel that makes building dynamic interfaces simple without leaving the +> comfort of Laravel. Laravel Data works excellently with [Laravel Livewire](https://laravel-livewire.com). @@ -46,3 +47,83 @@ class SongData extends Data implements Wireable } } ``` + +## Livewire Synths (Experimental) + +Laravel Data also provides a way to use Livewire Synths with your data objects. It will allow you to use data objects +and collections +without the need to make them Wireable. This is an experimental feature and is subject to change. + +You can enable this feature by setting the config option in `data.php`: + +```php +'livewire' => [ + 'enable_synths' => false, +] +``` + +Once enabled, you can use data objects within your Livewire components without the need to make them Wireable: + +```php +class SongUpdateComponent extends Component +{ + public SongData $data; + + public function mount(public int $id): void + { + $this->data = SongData::from(Song::findOrFail($id)); + } + + public function save(): void + { + Artist::findOrFail($this->id)->update($this->data->toArray()); + } + + public function render(): string + { + return <<<'BLADE' +
+

Songs

+ + +

Title: {{ $data->title }}

+

Artist: {{ $data->artist }}

+ +
+ BLADE; + } +} +``` + +### Lazy + +It is possible to use Lazy properties, these properties will not be sent over the wire unless they're included. **Always +include properties permanently** because a data object is being transformed and then cast again between Livewire +requests the includes should be permanent. + +It is possible to query lazy nested data objects, it is however not possible to query lazy properties which are not a data: + +```php +use Spatie\LaravelData\Lazy;class LazySongData extends Data +{ + public function __construct( + public Lazy|ArristData $artist, + public Lazy|string $title, + ) {} +} +``` + +Within your Livewire view + +```php +$this->data->artist->name; // Works +$this->data->title; // Does not work +``` + +### Validation + +Laravel data **does not provide validation** when using Livewire, you should do this yourself! This is because laravel-data +does not support object validation at the moment. Only validating payloads which eventually become data objects. +The validation could technically happen when hydrating the data object, but this is not implemented +because we cannot guarantee that every hydration happens when a user made sure the data is valid +and thus the payload should be validated. diff --git a/src/Concerns/ContextableData.php b/src/Concerns/ContextableData.php index 3feb8848..7af16360 100644 --- a/src/Concerns/ContextableData.php +++ b/src/Concerns/ContextableData.php @@ -72,4 +72,12 @@ public function getDataContext(): DataContext return $this->_dataContext; } + + public function setDataContext( + DataContext $dataContext + ): static { + $this->_dataContext = $dataContext; + + return $this; + } } diff --git a/src/LaravelDataServiceProvider.php b/src/LaravelDataServiceProvider.php index d9b648af..7b0f1142 100644 --- a/src/LaravelDataServiceProvider.php +++ b/src/LaravelDataServiceProvider.php @@ -53,7 +53,7 @@ function () { ); }); - if(class_exists(Livewire::class)) { + if(config('data.livewire.enable_synths') && class_exists(Livewire::class)) { $this->registerLivewireSynths(); } } diff --git a/src/Support/DataClassMorphMap.php b/src/Support/DataClassMorphMap.php index d1e604a7..8e3d5866 100644 --- a/src/Support/DataClassMorphMap.php +++ b/src/Support/DataClassMorphMap.php @@ -39,6 +39,9 @@ public function merge(array $map): self return $this; } + /** + * @return class-string|null + */ public function getMorphedDataClass(string $alias): ?string { return $this->map[$alias] ?? null; diff --git a/src/Support/Livewire/LivewireDataSynth.php b/src/Support/Livewire/LivewireDataSynth.php index 78b306dd..031bf6b2 100644 --- a/src/Support/Livewire/LivewireDataSynth.php +++ b/src/Support/Livewire/LivewireDataSynth.php @@ -5,36 +5,18 @@ use Livewire\Mechanisms\HandleComponents\ComponentContext; use Livewire\Mechanisms\HandleComponents\Synthesizers\Synth; use Spatie\LaravelData\Contracts\BaseData; +use Spatie\LaravelData\Contracts\ContextableData; use Spatie\LaravelData\Contracts\TransformableData; +use Spatie\LaravelData\Support\Creation\CreationContextFactory; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\Lazy\LivewireLostLazy; +use Spatie\LaravelData\Support\Transformation\TransformationContextFactory; class LivewireDataSynth extends Synth { - // TODO: - // - // Think about Lazy, we can always send them down? - // Or only conditional lazy which are true -> probably a better default - // What if we want to create a new data object and don't have the lazyvalue - // -> we could create a new LiveWireMissingLazy object, which we then use as the lazy value - // -> when resolving it would throw an error saying the value was lost in LiveWire - // - // Problem with computed properties should be sorted out - // - // DataCollections synth from the PR - // - // Do we want livewire as an dependency? - // - // Update docs - // - // Can we test this? - // - // Mapping property names, should we do this? - - protected DataConfig $dataConfig; - public static string $key = 'laravel-data-object'; + public static string $key = 'ldo'; public function __construct(ComponentContext $context, $path) { @@ -43,7 +25,7 @@ public function __construct(ComponentContext $context, $path) parent::__construct($context, $path); } - public static function match($target) + public static function match($target): bool { return $target instanceof BaseData && $target instanceof TransformableData; } @@ -59,20 +41,24 @@ public function set(&$target, $key, $value): void } /** - * @param BaseData&TransformableData $target + * @param BaseData&TransformableData&ContextableData $target * @param callable(mixed):mixed $dehydrateChild * * @return array */ public function dehydrate( - BaseData&TransformableData $target, + BaseData&TransformableData&ContextableData $target, callable $dehydrateChild ): array { $morph = $this->dataConfig->morphMap->getDataClassAlias($target::class) ?? $target::class; - $payload = $target->all(); - - ray($payload); + $payload = $target->transform( + TransformationContextFactory::create() + ->withPropertyNameMapping(false) + ->withoutWrapping() + ->withoutPropertyNameMapping() + ->withoutValueTransformation() + ); foreach ($payload as $key => $value) { $payload[$key] = $dehydrateChild($key, $value); @@ -80,7 +66,7 @@ public function dehydrate( return [ $payload, - ['morph' => $morph], + ['morph' => $morph, 'context' => encrypt($target->getDataContext())], ]; } @@ -97,6 +83,7 @@ public function hydrate( callable $hydrateChild ): BaseData { $morph = $meta['morph']; + $context = decrypt($meta['context']); $dataClass = $this->dataConfig->morphMap->getMorphedDataClass($morph) ?? $morph; @@ -112,8 +99,17 @@ public function hydrate( $payload[$name] = $hydrateChild($name, $value[$name]); } - return $dataClass::factory() + /** @var CreationContextFactory $factory */ + $factory = $dataClass::factory(); + + $data = $factory + ->withPropertyNameMapping(false) ->ignoreMagicalMethod('fromLivewire') + ->withoutValidation() ->from($payload); + + $data->setDataContext($context); + + return $data; } } diff --git a/src/Support/Partials/Partial.php b/src/Support/Partials/Partial.php index 615844c0..5d473b2a 100644 --- a/src/Support/Partials/Partial.php +++ b/src/Support/Partials/Partial.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support\Partials; use Closure; +use Laravel\SerializableClosure\SerializableClosure; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Support\Partials\Segments\AllPartialSegment; @@ -236,6 +237,30 @@ public function toArray(): array ]; } + public function toSerializedArray(): array + { + return [ + 'segments' => $this->segments, + 'permanent' => $this->permanent, + 'condition' => $this->condition + ? serialize(new SerializableClosure($this->condition)) + : null, + 'pointer' => $this->pointer, + ]; + } + + public static function fromSerializedArray(array $partial): Partial + { + return new self( + segments: $partial['segments'], + permanent: $partial['permanent'], + condition: $partial['condition'] + ? unserialize($partial['condition'])->getClosure() + : null, + pointer: $partial['pointer'], + ); + } + public function __toString(): string { return implode('.', $this->segments)." (current: {$this->pointer})"; diff --git a/src/Support/Partials/PartialsCollection.php b/src/Support/Partials/PartialsCollection.php index c3d47fb1..287ab79e 100644 --- a/src/Support/Partials/PartialsCollection.php +++ b/src/Support/Partials/PartialsCollection.php @@ -32,6 +32,17 @@ public function toArray(): array return $output; } + public function toSerializedArray(): array + { + $output = []; + + foreach ($this as $partial) { + $output[] = $partial->toSerializedArray(); + } + + return $output; + } + public function __toString(): string { $output = ''; @@ -42,4 +53,14 @@ public function __toString(): string return $output; } + + public static function fromSerializedArray(array $collection): PartialsCollection + { + return self::create( + ...array_map( + fn (array $partial) => Partial::fromSerializedArray($partial), + $collection + ) + ); + } } diff --git a/src/Support/Transformation/DataContext.php b/src/Support/Transformation/DataContext.php index 52c0d7b0..80c7f58c 100644 --- a/src/Support/Transformation/DataContext.php +++ b/src/Support/Transformation/DataContext.php @@ -68,4 +68,36 @@ public function getRequiredPartialsAndRemoveTemporaryOnes( return $requiredPartials; } + + public function toSerializedArray(): array + { + return [ + 'includePartials' => $this->includePartials?->toSerializedArray(), + 'excludePartials' => $this->excludePartials?->toSerializedArray(), + 'onlyPartials' => $this->onlyPartials?->toSerializedArray(), + 'exceptPartials' => $this->exceptPartials?->toSerializedArray(), + 'wrap' => $this->wrap?->toSerializedArray(), + ]; + } + + public static function fromSerializedArray(array $content): DataContext + { + return new self( + includePartials: $content['includePartials'] + ? PartialsCollection::fromSerializedArray($content['includePartials']) + : null, + excludePartials: $content['excludePartials'] + ? PartialsCollection::fromSerializedArray($content['excludePartials']) + : null, + onlyPartials: $content['onlyPartials'] + ? PartialsCollection::fromSerializedArray($content['onlyPartials']) + : null, + exceptPartials: $content['exceptPartials'] + ? PartialsCollection::fromSerializedArray($content['exceptPartials']) + : null, + wrap: $content['wrap'] + ? Wrap::fromSerializedArray($content['wrap']) + : null, + ); + } } diff --git a/src/Support/Wrapping/Wrap.php b/src/Support/Wrapping/Wrap.php index bf4ea049..49a440e0 100644 --- a/src/Support/Wrapping/Wrap.php +++ b/src/Support/Wrapping/Wrap.php @@ -33,4 +33,20 @@ public function getKey(): null|string default => throw new TypeError('Invalid wrap') }; } + + public function toSerializedArray(): array + { + return [ + 'type' => $this->type->value, + 'key' => $this->key, + ]; + } + + public static function fromSerializedArray(array $wrap): Wrap + { + return new Wrap( + type: WrapType::from($wrap['type']), + key: $wrap['key'] ?? null, + ); + } } diff --git a/src/Support/Wrapping/WrapType.php b/src/Support/Wrapping/WrapType.php index 8b35a38f..3aa2ea39 100644 --- a/src/Support/Wrapping/WrapType.php +++ b/src/Support/Wrapping/WrapType.php @@ -2,9 +2,9 @@ namespace Spatie\LaravelData\Support\Wrapping; -enum WrapType +enum WrapType: string { - case UseGlobal; - case Disabled; - case Defined; + case UseGlobal = 'use_global'; + case Disabled = 'disabled'; + case Defined = 'defined'; } diff --git a/tests/Fakes/ComputedData.php b/tests/Fakes/ComputedData.php new file mode 100644 index 00000000..3b9ce3fb --- /dev/null +++ b/tests/Fakes/ComputedData.php @@ -0,0 +1,17 @@ +name = $this->first_name . ' ' . $this->last_name; + } +} diff --git a/tests/Fakes/Livewire/ComputedDataComponent.php b/tests/Fakes/Livewire/ComputedDataComponent.php new file mode 100644 index 00000000..60d497f7 --- /dev/null +++ b/tests/Fakes/Livewire/ComputedDataComponent.php @@ -0,0 +1,33 @@ +data = new ComputedData('', ''); + } + + public function save() + { + // Trigger hydration + } + + public function render() + { + return <<<'BLADE' +
+ + + +

{{ $data->name }}

+
+ BLADE; + } +} diff --git a/tests/Fakes/Livewire/MappedDataComponent.php b/tests/Fakes/Livewire/MappedDataComponent.php new file mode 100644 index 00000000..a9c75e68 --- /dev/null +++ b/tests/Fakes/Livewire/MappedDataComponent.php @@ -0,0 +1,32 @@ +data = new SimpleDataWithMappedProperty('Hello World'); + } + + public function save() + { + cache()->set('name', $this->data->string); + } + + public function render() + { + return <<<'BLADE' +
+ +

{{ $data->string }}

+ +
+ BLADE; + } +} diff --git a/tests/Fakes/Livewire/NestedDataComponent.php b/tests/Fakes/Livewire/NestedDataComponent.php new file mode 100644 index 00000000..7398a528 --- /dev/null +++ b/tests/Fakes/Livewire/NestedDataComponent.php @@ -0,0 +1,36 @@ +data = new NestedLazyData($nested); + + $this->data->includePermanently(...$includes); + } + + public function save() + { + cache()->set('name', $this->data->simple->string); + } + + public function render() + { + return <<<'BLADE' +
+ +

{{ $data->simple->string }}

+ +
+ BLADE; + } +} diff --git a/tests/Fakes/Livewire/SimpleDataComponent.php b/tests/Fakes/Livewire/SimpleDataComponent.php new file mode 100644 index 00000000..2651e8b9 --- /dev/null +++ b/tests/Fakes/Livewire/SimpleDataComponent.php @@ -0,0 +1,35 @@ +data = new LazyData($name); + + $this->data->includePermanently(...$includes); + } + + public function save() + { + cache()->set('name', $this->data->name); + } + + public function render() + { + return <<<'BLADE' +
+ +

{{ is_string($data->name) ? $data->name : 'lazy prop' }}

+ +
+ BLADE; + } +} diff --git a/tests/LivewireTest.php b/tests/LivewireTest.php index 52777f12..d656bd52 100644 --- a/tests/LivewireTest.php +++ b/tests/LivewireTest.php @@ -1,7 +1,19 @@ toLivewire())->toEqual(['name' => 'Freek']); }); + +describe('synth tests', function () { + beforeEach(function () { + app()->register(LivewireServiceProvider::class); + + Livewire::propertySynthesizer(LivewireDataSynth::class); + }); + + it('can initialize a data object', function () { + livewire(SimpleDataComponent::class, ['name' => 'Hello World']) + ->assertSet('data.name', 'Hello World'); + }); + + it('can set a data object property', function () { + livewire(SimpleDataComponent::class, ['name' => 'Hello World']) + ->set('data.name', 'Hello World from Livewire') + ->assertSet('data.name', 'Hello World from Livewire') + ->assertSee('Hello World from Livewire'); + }); + + it('will not send lazy data to the front when not included', function () { + livewire(SimpleDataComponent::class, ['name' => Lazy::create(fn () => 'Hello World')]) + ->assertDontSee('Hello World'); + }); + + it('is possible to set included lazy data', function () { + livewire(SimpleDataComponent::class, ['name' => Lazy::create(fn () => 'Hello World'), 'includes' => ['name']]) + ->assertDontSee('Hello World') + ->set('data.name', 'Hello World from Livewire') + ->assertSet('data.name', 'Hello World from Livewire') + ->assertSee('Hello World from Livewire'); + }); + + it('can initialize a nested data object', function () { + livewire(NestedDataComponent::class, ['nested' => new SimpleData('Hello World')]) + ->assertSet('data.simple.string', 'Hello World'); + }); + + it('can set a nested data object property', function () { + livewire(NestedDataComponent::class, ['nested' => new SimpleData('Hello World')]) + ->set('data.simple.string', 'Hello World from Livewire') + ->assertSet('data.simple.string', 'Hello World from Livewire') + ->assertSee('Hello World from Livewire'); + }); + + it('will not map property names', function () { + livewire(MappedDataComponent::class) + ->set('data.string', 'Hello World from Livewire') + ->assertSet('data.string', 'Hello World from Livewire') + ->assertSee('Hello World from Livewire'); + }); + + it('can use computed properties', function () { + livewire(ComputedDataComponent::class) + ->set('data.first_name', 'Ruben') + ->assertSet('data.first_name', 'Ruben') + ->assertSet('data.name', ' ') // Computed properties only rerender after constructor calls + ->assertSee(' ') + ->set('data.last_name', 'Van Assche') + ->assertSet('data.last_name', 'Van Assche') + ->call('save'); + }); +}); diff --git a/tests/Support/Transformation/DataContextTest.php b/tests/Support/Transformation/DataContextTest.php new file mode 100644 index 00000000..3828bf54 --- /dev/null +++ b/tests/Support/Transformation/DataContextTest.php @@ -0,0 +1,54 @@ + 'Check that this is true'), + Partial::create('nested.field'), + Partial::create('*'), + Partial::create('nested.field.pointed')->next(), + ), + excludePartials: null, + onlyPartials: null, + exceptPartials: null, + wrap: new Wrap(type: WrapType::Defined, key: 'key'), + ); + + $serializable = $context->toSerializedArray(); + + $serialized = serialize($serializable); + + $unserialized = unserialize($serialized); + + $deserialized = DataContext::fromSerializedArray($unserialized); + + expect($deserialized) + ->toBeInstanceOf(DataContext::class) + ->includePartials->toHaveCount(6) + ->excludePartials->toBeNull() + ->onlyPartials->toBeNull() + ->exceptPartials->toBeNull() + ->wrap->toEqual($context->wrap); + + $partials = $deserialized->includePartials->toArray(); + $expectedPartials = $context->includePartials->toArray(); + + expect($partials[0])->toEqual($expectedPartials[0]); + expect($partials[1])->toEqual($expectedPartials[1]); + expect($partials[0])->toEqual($expectedPartials[0]); + expect($partials[3])->toEqual($expectedPartials[3]); + expect($partials[4])->toEqual($expectedPartials[4]); + expect($partials[5])->toEqual($expectedPartials[5]); + + expect($partials[2]['condition']())->toEqual($expectedPartials[2]['condition']()); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 0d66d9d7..2625cf1e 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,6 +12,8 @@ protected function setUp(): void { parent::setUp(); + config()->set('app.key', 'base64:'.base64_encode(random_bytes(32))); + Model::unguard(); } From 121c70842eba0409d0ddcf75e982f36a4eb0f181 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Mar 2024 13:12:22 +0100 Subject: [PATCH 6/7] Fix PHPStan --- src/Support/Livewire/LivewireDataSynth.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Support/Livewire/LivewireDataSynth.php b/src/Support/Livewire/LivewireDataSynth.php index 031bf6b2..10109dd9 100644 --- a/src/Support/Livewire/LivewireDataSynth.php +++ b/src/Support/Livewire/LivewireDataSynth.php @@ -41,10 +41,7 @@ public function set(&$target, $key, $value): void } /** - * @param BaseData&TransformableData&ContextableData $target - * @param callable(mixed):mixed $dehydrateChild - * - * @return array + * @param callable(array-key, mixed):mixed $dehydrateChild */ public function dehydrate( BaseData&TransformableData&ContextableData $target, @@ -71,11 +68,7 @@ public function dehydrate( } /** - * @param mixed $value - * @param array $meta - * @param callable(mixed):mixed $hydrateChild - * - * @return BaseData + * @param callable(array-key, mixed):mixed $hydrateChild */ public function hydrate( array $value, From ad4ad2ec5d8961c7d9da685a5358c49a88052759 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 1 Mar 2024 14:44:02 +0100 Subject: [PATCH 7/7] Rewrite collection synth --- src/LaravelDataServiceProvider.php | 2 + .../Livewire/LivewireDataCollectionSynth.php | 82 +++++++++++++++++++ src/Synths/DataCollectionSynth.php | 55 ------------- .../Livewire/DataCollectionComponent.php | 39 +++++++++ tests/LivewireTest.php | 15 ++++ 5 files changed, 138 insertions(+), 55 deletions(-) create mode 100644 src/Support/Livewire/LivewireDataCollectionSynth.php delete mode 100644 src/Synths/DataCollectionSynth.php create mode 100644 tests/Fakes/Livewire/DataCollectionComponent.php diff --git a/src/LaravelDataServiceProvider.php b/src/LaravelDataServiceProvider.php index 7b0f1142..b5871384 100644 --- a/src/LaravelDataServiceProvider.php +++ b/src/LaravelDataServiceProvider.php @@ -8,6 +8,7 @@ use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Support\Caching\DataStructureCache; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\Livewire\LivewireDataCollectionSynth; use Spatie\LaravelData\Support\Livewire\LivewireDataSynth; use Spatie\LaravelData\Support\VarDumper\VarDumperManager; use Spatie\LaravelPackageTools\Package; @@ -61,6 +62,7 @@ function () { protected function registerLivewireSynths(): void { Livewire::propertySynthesizer(LivewireDataSynth::class); + Livewire::propertySynthesizer(LivewireDataCollectionSynth::class); } public function packageBooted(): void diff --git a/src/Support/Livewire/LivewireDataCollectionSynth.php b/src/Support/Livewire/LivewireDataCollectionSynth.php new file mode 100644 index 00000000..9954fab7 --- /dev/null +++ b/src/Support/Livewire/LivewireDataCollectionSynth.php @@ -0,0 +1,82 @@ +dataConfig = app(DataConfig::class); + + parent::__construct($context, $path); + } + + public static function match($target): bool + { + return is_a($target, DataCollection::class, true); + } + + public function get(&$target, $key): BaseData + { + return $target[$key]; + } + + public function set(&$target, $key, $value) + { + $target[$key] = $value; + } + + /** + * @param callable(array-key, mixed):mixed $dehydrateChild + */ + public function dehydrate(DataCollection $target, callable $dehydrateChild): array + { + $morph = $this->dataConfig->morphMap->getDataClassAlias($target->dataClass) ?? $target->dataClass; + + $payload = []; + + foreach ($target->toCollection() as $key => $child) { + $payload[$key] = $dehydrateChild($key, $child); + } + + return [ + $payload, + [ + 'dataCollectionClass' => $target::class, + 'dataMorph' => $morph, + 'context' => encrypt($target->getDataContext()), + ], + ]; + } + + /** + * @param callable(array-key, mixed):mixed $hydrateChild + */ + public function hydrate($value, $meta, $hydrateChild) + { + $context = decrypt($meta['context']); + $dataCollectionClass = $meta['dataCollectionClass']; + $dataClass = $this->dataConfig->morphMap->getMorphedDataClass($meta['dataMorph']) ?? $meta['dataMorph']; + + foreach ($value as $key => $child) { + $value[$key] = $hydrateChild($key, $child); + } + + /** @var DataCollection $dataCollection */ + $dataCollection = new $dataCollectionClass($dataClass, $value); + + $dataCollection->setDataContext($context); + + return $dataCollection; + } +} diff --git a/src/Synths/DataCollectionSynth.php b/src/Synths/DataCollectionSynth.php deleted file mode 100644 index c4591eb5..00000000 --- a/src/Synths/DataCollectionSynth.php +++ /dev/null @@ -1,55 +0,0 @@ -all(); - - foreach ($data as $key => $child) { - $data[$key] = $dehydrateChild($key, $child); - } - - return [ - $data, - ['class' => get_class($target)], - ]; - } - - /** - * @param array $value - * @param array $meta - * @param mixed $hydrateChild - * @return \Spatie\LaravelData\DataCollection - */ - public function hydrate($value, $meta, $hydrateChild) - { - foreach ($value as $key => $child) { - $value[$key] = $hydrateChild($key, $child); - } - - return $meta['class']::make($value); - } - - public function get(&$target, $key) - { - return $target[$key] ?? null; - } - - public function set(&$target, $key, $value) - { - $target[$key] = $value; - } -} diff --git a/tests/Fakes/Livewire/DataCollectionComponent.php b/tests/Fakes/Livewire/DataCollectionComponent.php new file mode 100644 index 00000000..82a4e3b4 --- /dev/null +++ b/tests/Fakes/Livewire/DataCollectionComponent.php @@ -0,0 +1,39 @@ +collection = SimpleData::collect([ + 'a', 'b', 'c', + ], DataCollection::class); + } + + public function save() + { + } + + public function render() + { + return <<<'BLADE' +
+ @foreach($collection as $item) +

{{ $item->string }}

+ @endforeach + +

{{ $collection[0]->string }}

+ +
+ BLADE; + } +} diff --git a/tests/LivewireTest.php b/tests/LivewireTest.php index d656bd52..61f62bf6 100644 --- a/tests/LivewireTest.php +++ b/tests/LivewireTest.php @@ -7,9 +7,12 @@ use Spatie\LaravelData\Concerns\WireableData; use Spatie\LaravelData\Data; + use Spatie\LaravelData\Lazy; +use Spatie\LaravelData\Support\Livewire\LivewireDataCollectionSynth; use Spatie\LaravelData\Support\Livewire\LivewireDataSynth; use Spatie\LaravelData\Tests\Fakes\Livewire\ComputedDataComponent; +use Spatie\LaravelData\Tests\Fakes\Livewire\DataCollectionComponent; use Spatie\LaravelData\Tests\Fakes\Livewire\MappedDataComponent; use Spatie\LaravelData\Tests\Fakes\Livewire\NestedDataComponent; use Spatie\LaravelData\Tests\Fakes\Livewire\SimpleDataComponent; @@ -37,6 +40,7 @@ public function __construct( app()->register(LivewireServiceProvider::class); Livewire::propertySynthesizer(LivewireDataSynth::class); + Livewire::propertySynthesizer(LivewireDataCollectionSynth::class); }); it('can initialize a data object', function () { @@ -93,4 +97,15 @@ public function __construct( ->assertSet('data.last_name', 'Van Assche') ->call('save'); }); + + it('can use data collections', function () { + livewire(DataCollectionComponent::class) + ->assertSee('a') + ->assertSee('b') + ->assertSee('c') + ->set('collection.0.string', 'Hello World') + ->assertSet('collection.0.string', 'Hello World') + ->assertSee('Hello World') + ->call('save'); + }); });