From 088b4fd8258646271b8507e2fff3b8908e810561 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Fri, 3 Jan 2025 17:17:04 +0100 Subject: [PATCH] Fix an issue where anonymous classes in castables were serialized --- src/Attributes/WithCastable.php | 6 ++- src/Casts/CastableCast.php | 43 +++++++++++++++++++ src/Support/Factories/DataClassFactory.php | 1 - .../Support/Caching/CachedDataConfigTest.php | 32 ++++++++++++++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/Casts/CastableCast.php diff --git a/src/Attributes/WithCastable.php b/src/Attributes/WithCastable.php index b82c5c81e..b3fdc66c5 100644 --- a/src/Attributes/WithCastable.php +++ b/src/Attributes/WithCastable.php @@ -5,6 +5,7 @@ use Attribute; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Casts\Castable; +use Spatie\LaravelData\Casts\CastableCast; use Spatie\LaravelData\Exceptions\CannotCreateCastAttribute; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)] @@ -26,6 +27,9 @@ public function __construct( public function get(): Cast { - return $this->castableClass::dataCastUsing(...$this->arguments); + return new CastableCast( + $this->castableClass, + $this->arguments + ); } } diff --git a/src/Casts/CastableCast.php b/src/Casts/CastableCast.php new file mode 100644 index 000000000..e28ae7427 --- /dev/null +++ b/src/Casts/CastableCast.php @@ -0,0 +1,43 @@ + $castableClass + */ + public function __construct( + public string $castableClass, + public array $arguments + ) { + } + + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed + { + if (! isset($this->cast)) { + $this->cast = $this->castableClass::dataCastUsing(...$this->arguments); + } + + return $this->cast->cast($property, $value, $properties, $context); + } + + public function __serialize(): array + { + return [ + 'castableClass' => $this->castableClass, + 'arguments' => $this->arguments, + ]; + } + + public function __unserialize(array $data): void + { + $this->castableClass = $data['castableClass']; + $this->arguments = $data['arguments']; + } +} diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php index 46b09a2ca..7082a36e5 100644 --- a/src/Support/Factories/DataClassFactory.php +++ b/src/Support/Factories/DataClassFactory.php @@ -33,7 +33,6 @@ public function __construct( ) { } - public function build(ReflectionClass $reflectionClass): DataClass { /** @var class-string $name */ diff --git a/tests/Support/Caching/CachedDataConfigTest.php b/tests/Support/Caching/CachedDataConfigTest.php index 732031d5b..b9e5535c8 100644 --- a/tests/Support/Caching/CachedDataConfigTest.php +++ b/tests/Support/Caching/CachedDataConfigTest.php @@ -3,11 +3,14 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Cache; use Mockery\MockInterface; +use Spatie\LaravelData\Attributes\WithCastable; +use Spatie\LaravelData\Data; use Spatie\LaravelData\Support\Caching\CachedDataConfig; 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\Castables\SimpleCastable; use Spatie\LaravelData\Tests\Fakes\SimpleData; function ensureDataWillBeCached() @@ -104,3 +107,32 @@ function (MockInterface $spy) use ($dataClass) { cache()->get('something-just-to-test-the-mock'); }); + +it('is possible to cache data classes with castables using anonymous classes', function () { + ensureDataWillBeCached(); + + $objectDefinition = new class () extends Data { + #[WithCastable(SimpleCastable::class, normalize: true)] + public SimpleCastable $string; + }; + + $dataClass = app(DataConfig::class)->getDataClass($objectDefinition::class); + + expect(isset(invade($dataClass->properties['string']->cast)->cast))->toBeFalse(); + + $objectDefinition::from(['string' => 'Hello world']); + + $dataClass = app(DataConfig::class)->getDataClass($objectDefinition::class); + + $reflection = new ReflectionClass(invade($dataClass->properties['string']->cast)->cast); + + expect($reflection->isAnonymous())->toBeTrue(); + + $dataClass->prepareForCache(); + + app(DataStructureCache::class)->storeDataClass($dataClass); + + $newDataClass = app(DataStructureCache::class)->getDataClass($objectDefinition::class); + + expect(isset(invade($newDataClass->properties['string']->cast)->cast))->toBeFalse(); +});