diff --git a/src/Concerns/BaseData.php b/src/Concerns/BaseData.php index beac3bdf..ba342e64 100644 --- a/src/Concerns/BaseData.php +++ b/src/Concerns/BaseData.php @@ -90,6 +90,7 @@ public function __sleep(): array ->properties ->map(fn (DataProperty $property) => $property->name) ->when($dataClass->appendable, fn (Collection $properties) => $properties->push('_additional')) + ->when(property_exists($this, '_dataContext'), fn (Collection $properties) => $properties->push('_dataContext')) ->toArray(); } } diff --git a/src/Lazy.php b/src/Lazy.php index 621b6d5c..d5b2ef52 100644 --- a/src/Lazy.php +++ b/src/Lazy.php @@ -44,6 +44,10 @@ public static function closure(Closure $closure): ClosureLazy abstract public function resolve(): mixed; + abstract public function __serialize(): array; + + abstract public function __unserialize(array $data): void; + public function defaultIncluded(bool $defaultIncluded = true): self { $this->defaultIncluded = $defaultIncluded; diff --git a/src/Support/Lazy/ConditionalLazy.php b/src/Support/Lazy/ConditionalLazy.php index ba8e9ff4..13625b25 100644 --- a/src/Support/Lazy/ConditionalLazy.php +++ b/src/Support/Lazy/ConditionalLazy.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support\Lazy; use Closure; +use Laravel\SerializableClosure\SerializableClosure; use Spatie\LaravelData\Lazy; class ConditionalLazy extends Lazy @@ -22,4 +23,20 @@ public function shouldBeIncluded(): bool { return (bool) ($this->condition)(); } + + public function __serialize(): array + { + return [ + 'condition' => new SerializableClosure($this->condition), + 'value' => new SerializableClosure($this->value), + 'defaultIncluded' => $this->defaultIncluded, + ]; + } + + public function __unserialize(array $data): void + { + $this->condition = $data['condition']->getClosure(); + $this->value = $data['value']->getClosure(); + $this->defaultIncluded = $data['defaultIncluded']; + } } diff --git a/src/Support/Lazy/DefaultLazy.php b/src/Support/Lazy/DefaultLazy.php index d50bb26f..7f82ff91 100644 --- a/src/Support/Lazy/DefaultLazy.php +++ b/src/Support/Lazy/DefaultLazy.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support\Lazy; use Closure; +use Laravel\SerializableClosure\SerializableClosure; use Spatie\LaravelData\Lazy; class DefaultLazy extends Lazy @@ -16,4 +17,18 @@ public function resolve(): mixed { return ($this->value)(); } + + public function __serialize(): array + { + return [ + 'value' => new SerializableClosure($this->value), + 'defaultIncluded' => $this->defaultIncluded, + ]; + } + + public function __unserialize(array $data): void + { + $this->value = $data['value']->getClosure(); + $this->defaultIncluded = $data['defaultIncluded']; + } } diff --git a/src/Support/Lazy/LivewireLostLazy.php b/src/Support/Lazy/LivewireLostLazy.php index 2e945241..7d639339 100644 --- a/src/Support/Lazy/LivewireLostLazy.php +++ b/src/Support/Lazy/LivewireLostLazy.php @@ -17,4 +17,20 @@ public function resolve(): mixed { return throw new Exception("Lazy property `{$this->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."); } + + public function __serialize(): array + { + return [ + 'dataClass' => $this->dataClass, + 'propertyName' => $this->propertyName, + 'defaultIncluded' => $this->defaultIncluded, + ]; + } + + public function __unserialize(array $data): void + { + $this->dataClass = $data['dataClass']; + $this->propertyName = $data['propertyName']; + $this->defaultIncluded = $data['defaultIncluded']; + } } diff --git a/src/Support/Lazy/RelationalLazy.php b/src/Support/Lazy/RelationalLazy.php index 703d0aa7..24240e7f 100644 --- a/src/Support/Lazy/RelationalLazy.php +++ b/src/Support/Lazy/RelationalLazy.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Database\Eloquent\Model; +use Laravel\SerializableClosure\SerializableClosure; use Spatie\LaravelData\Lazy; class RelationalLazy extends Lazy @@ -24,4 +25,22 @@ public function shouldBeIncluded(): bool { return $this->model->relationLoaded($this->relation); } + + public function __serialize(): array + { + return [ + 'relation' => $this->relation, + 'model' => $this->model, + 'value' => new SerializableClosure($this->value), + 'defaultIncluded' => $this->defaultIncluded, + ]; + } + + public function __unserialize(array $data): void + { + $this->relation = $data['relation']; + $this->model = $data['model']; + $this->value = $data['value']->getClosure(); + $this->defaultIncluded = $data['defaultIncluded']; + } } diff --git a/src/Support/Partials/Partial.php b/src/Support/Partials/Partial.php index 6f5fe24f..9c7561b4 100644 --- a/src/Support/Partials/Partial.php +++ b/src/Support/Partials/Partial.php @@ -265,4 +265,30 @@ public function __toString(): string { return implode('.', $this->segments)." (current: {$this->pointer})"; } + + public function __serialize(): array + { + return [ + 'segmentCount' => $this->segmentCount, + 'endsInAll' => $this->endsInAll, + 'segments' => $this->segments, + 'condition' => $this->condition + ? serialize(new SerializableClosure($this->condition)) + : null, + 'permanent' => $this->permanent, + 'pointer' => $this->pointer, + ]; + } + + public function __unserialize(array $data): void + { + $this->segmentCount = $data['segmentCount']; + $this->endsInAll = $data['endsInAll']; + $this->segments = $data['segments']; + $this->pointer = $data['pointer']; + $this->condition = $data['condition'] + ? unserialize($data['condition'])->getClosure() + : null; + $this->permanent = $data['permanent']; + } } diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index a1f3db9d..63f75c80 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -10,8 +10,6 @@ use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\SimpleData; -use function Spatie\Snapshots\assertMatchesSnapshot; - it('can filter a collection', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); @@ -237,35 +235,6 @@ ->toMatchArray($filtered); }); -it('can serialize and unserialize a data collection', function () { - $collection = new DataCollection(SimpleData::class, ['A', 'B']); - - $serialized = serialize($collection); - - assertMatchesSnapshot($serialized); - - $unserialized = unserialize($serialized); - - expect($unserialized)->toBeInstanceOf(DataCollection::class); - expect($unserialized)->toEqual(new DataCollection(SimpleData::class, ['A', 'B'])); -}); - -it('during the serialization process some properties are thrown away', function () { - $collection = new DataCollection(SimpleData::class, ['A', 'B']); - - $collection->include('test'); - $collection->exclude('test'); - $collection->only('test'); - $collection->except('test'); - $collection->wrap('test'); - - $unserialized = unserialize(serialize($collection)); - - $invaded = invade($unserialized); - - 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'], diff --git a/tests/DataTest.php b/tests/DataTest.php index 47edb71c..95a616bd 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -22,12 +22,9 @@ use Spatie\LaravelData\Data; use Spatie\LaravelData\Dto; use Spatie\LaravelData\Resource; -use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Tests\Fakes\SimpleDto; use Spatie\LaravelData\Tests\Fakes\SimpleResource; -use function Spatie\Snapshots\assertMatchesSnapshot; - it('also works by using traits and interfaces, skipping the base data class', function () { $data = new class ('') implements Responsable, AppendableDataContract, BaseDataContract, TransformableDataContract, IncludeableDataContract, ResponsableDataContract, ValidateableDataContract, WrappableDataContract, EmptyDataContract { use ResponsableData; @@ -56,51 +53,6 @@ public static function fromString(string $string): static }); -it('can serialize and unserialize a data object', function () { - $object = SimpleData::from('Hello world'); - - $serialized = serialize($object); - - assertMatchesSnapshot($serialized); - - $unserialized = unserialize($serialized); - - expect($unserialized)->toBeInstanceOf(SimpleData::class); - expect($unserialized->string)->toEqual('Hello world'); -}); - -it('can serialize and unserialize a data object with additional data', function () { - $object = SimpleData::from('Hello world')->additional([ - 'int' => 69, - ]); - - $serialized = serialize($object); - - assertMatchesSnapshot($serialized); - - $unserialized = unserialize($serialized); - - expect($unserialized)->toBeInstanceOf(SimpleData::class); - expect($unserialized->string)->toEqual('Hello world'); - expect($unserialized->getAdditionalData())->toEqual(['int' => 69]); -}); - -it('during the serialization process some properties are thrown away', function () { - $object = SimpleData::from('Hello world'); - - $object->include('test'); - $object->exclude('test'); - $object->only('test'); - $object->except('test'); - $object->wrap('test'); - - $unserialized = unserialize(serialize($object)); - - $invaded = invade($unserialized); - - expect($invaded->_dataContext)->toBeNull(); -}); - it('can use data as an DTO', function () { $dto = SimpleDto::from('Hello World'); diff --git a/tests/SerializeableTest.php b/tests/SerializeableTest.php new file mode 100644 index 00000000..7d9bc8b3 --- /dev/null +++ b/tests/SerializeableTest.php @@ -0,0 +1,85 @@ +toBeInstanceOf(SimpleData::class); + expect($unserialized->string)->toEqual('Hello world'); +}); + +it('can serialize and unserialize a data object with additional data', function () { + $object = SimpleData::from('Hello world')->additional([ + 'int' => 69, + ]); + + $serialized = serialize($object); + + assertMatchesSnapshot($serialized); + + $unserialized = unserialize($serialized); + + expect($unserialized)->toBeInstanceOf(SimpleData::class); + expect($unserialized->string)->toEqual('Hello world'); + expect($unserialized->getAdditionalData())->toEqual(['int' => 69]); +}); + +it('can serialize and unserialize a data collection', function () { + $collection = new DataCollection(SimpleData::class, ['A', 'B']); + + $serialized = serialize($collection); + + assertMatchesSnapshot($serialized); + + $unserialized = unserialize($serialized); + + expect($unserialized)->toBeInstanceOf(DataCollection::class); + expect($unserialized)->toEqual(new DataCollection(SimpleData::class, ['A', 'B'])); +}); + +it('will keep context attached to data when serialized', function () { + $object = LazyData::from('Hello world')->include('name'); + + $unserialized = unserialize(serialize($object)); + + expect($unserialized)->toBeInstanceOf(LazyData::class); + expect($unserialized->toArray())->toMatchArray(['name' => 'Hello world']); +}); + +it('is possible to add partials with closures and serialize them', function () { + $object = LazyData::from('Hello world')->includeWhen( + 'name', + fn (LazyData $data) => $data->name instanceof DefaultLazy + ); + + $unserialized = unserialize(serialize($object)); + + expect($unserialized)->toBeInstanceOf(LazyData::class); + expect($unserialized->toArray())->toMatchArray(['name' => 'Hello world']); +}); + +it('is possible to serialize conditional lazy properties', function () { + $object = new LazyData(Lazy::when( + fn () => true, + fn () => 'Hello world' + )); + + $unserialized = unserialize(serialize($object)); + + expect($unserialized)->toBeInstanceOf(LazyData::class); + expect($unserialized->toArray())->toMatchArray(['name' => 'Hello world']); +}); diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name_on_a_property__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name_on_a_property__1.txt new file mode 100644 index 00000000..e5b50a34 --- /dev/null +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_outputs_types_with_properties_using_their_mapped_name_on_a_property__1.txt @@ -0,0 +1,4 @@ +{ +some_camel_case_property: string; +'some:non:standard:property': string; +} \ No newline at end of file diff --git a/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_collection__1.txt b/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_collection__1.txt new file mode 100644 index 00000000..835f8f95 Binary files /dev/null and b/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_collection__1.txt differ diff --git a/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_object__1.txt b/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_object__1.txt new file mode 100644 index 00000000..f7ab9b57 Binary files /dev/null and b/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_object__1.txt differ diff --git a/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_object_with_additional_data__1.txt b/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_object_with_additional_data__1.txt new file mode 100644 index 00000000..eec19beb Binary files /dev/null and b/tests/__snapshots__/SerializeableTest__it_can_serialize_and_unserialize_a_data_object_with_additional_data__1.txt differ