diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index be52633..f253f23 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -16,14 +16,11 @@ jobs: php: [ 8.1, 8.2 , 8.3 ] laravel: [ ^10.0, ^11.0 ] stability: [ prefer-stable, prefer-lowest ] - laravelData: [ ^3.0, ^4.0 ] exclude: - php: 8.1 laravel: ^11.0 - - laravel: ^11.0 - laravelData: ^3.0 - name: P${{ matrix.php }} - L${{ matrix.laravel }} - D${{ matrix.laravelData }} - ${{ matrix.stability }} + name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} steps: - name: Checkout code @@ -44,7 +41,6 @@ jobs: - name: Install dependencies run: | composer require "illuminate/contracts:${{ matrix.laravel }}" --no-interaction --no-update - composer require "spatie/laravel-data:${{ matrix.laravelData }}" --no-interaction --no-update composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress - name: Execute tests diff --git a/README.md b/README.md index e7070bc..41948e1 100644 --- a/README.md +++ b/README.md @@ -279,10 +279,35 @@ This package adds some laravel specific options for serialization/deserializatio - Eloquent models can be correctly serialized/deserialized (with relations) adding `TemporalSerializable` interface and `TemporalEloquentSerialize` trait. - [spatie/laravel-data](https://github.com/spatie/laravel-data) data objects are supported out of the box. -> To improve laravel-data support, this package provides `TemporalSerializableCastAndTransformer` (which implements cast and transformer for laravel-data). -> to add support for serialization/deserialization of `TemporalSerializable` objects used as `Data` properties. -> You can add them to `data.casts` and `data.transformers` config to add support globally, -> or use it with `WithCast`, `WithTransformer` attributes to add support to specific data objects (in v4 the attributes can be combined with `WithCastAndTransform`). +#### Spatie/Laravel-Data support + +`spatie/laravel-data` is a package that provides a simple way to work with data objects in Laravel. +In order to take full advantage of `laravel-data`, it is suggested to use `v4.3.0` or higher. + +> [!NOTE] +> The provided `TemporalSerializableCastAndTransformer` is compatible only with `laravel-data` `v4.3` or higher, +> if you are using an older version you can create your cast/transform. + +Changes to be made in `config/data.php`: + +```php + // Enable iterables cast/transform + 'features' => [ + 'cast_and_transform_iterables' => true, + ], + + // Add support for TemporalSerializable transform + 'transformers' => [ + //... + \Keepsuit\LaravelTemporal\Contracts\TemporalSerializable::class => \Keepsuit\LaravelTemporal\Integrations\LaravelData\TemporalSerializableCastAndTransformer::class, + ], + + // Add support for TemporalSerializable cast + 'casts' => [ + //... + \Keepsuit\LaravelTemporal\Contracts\TemporalSerializable::class => \Keepsuit\LaravelTemporal\Integrations\LaravelData\TemporalSerializableCastAndTransformer::class, + ], +``` ### Interceptors diff --git a/composer.json b/composer.json index ce32019..2edcc68 100644 --- a/composer.json +++ b/composer.json @@ -41,12 +41,12 @@ "phpstan/phpstan-phpunit": "^1.0", "rector/rector": "^1.0", "spatie/invade": "^2.0", - "spatie/laravel-data": "^4.0", + "spatie/laravel-data": "^4.3", "spatie/laravel-ray": "^1.26", "thecodingmachine/phpstan-safe-rule": "^1.2" }, "suggest": { - "spatie/laravel-data": "Can be used for workflows payloads (^3.0 || ^4.0)" + "spatie/laravel-data": "Can be used for workflows payloads (recommended ^4.3)" }, "autoload": { "psr-4": { diff --git a/src/Integrations/LaravelData/LaravelDataHelpers.php b/src/Integrations/LaravelData/LaravelDataHelpers.php deleted file mode 100644 index 4cd4a2c..0000000 --- a/src/Integrations/LaravelData/LaravelDataHelpers.php +++ /dev/null @@ -1,22 +0,0 @@ -explode('.') - ->first(); - } -} diff --git a/src/Integrations/LaravelData/LaravelDataTemporalSerializer.php b/src/Integrations/LaravelData/LaravelDataTemporalSerializer.php deleted file mode 100644 index bca38d3..0000000 --- a/src/Integrations/LaravelData/LaravelDataTemporalSerializer.php +++ /dev/null @@ -1,84 +0,0 @@ -map(fn (mixed $item) => $this->serializeItem($item))->toArray(); - } - - if (is_array($value)) { - return Arr::map($value, fn (mixed $item) => $this->serializeItem($item)); - } - - return $this->serializeItem($value); - } - - public function cast(DataProperty $property, mixed $value, ?string $type = null): mixed - { - $enumerableClass = $property->type->findAcceptedTypeForBaseType(\Illuminate\Support\Enumerable::class); - $isArrayOrCollection = $enumerableClass !== null || $property->type->acceptsType('array'); - - if ($isArrayOrCollection && $type === null) { - return Uncastable::create(); - } - - if ($isArrayOrCollection) { - $items = Arr::map($value, fn ($item) => rescue( - fn () => $this->deserializeItem($item, $type), - fn () => Uncastable::create(), - report: false - )); - - return $enumerableClass !== null ? $enumerableClass::make($items) : $items; - } - - $type = $this->type ?? $property->type->findAcceptedTypeForBaseType(TemporalSerializable::class); - - return rescue( - fn () => $this->deserializeItem($value, $type), - fn () => Uncastable::create(), - report: false - ); - } - - protected function serializeItem(mixed $item): mixed - { - if ($item instanceof TemporalSerializable) { - return $item->toTemporalPayload(); - } - - return $item; - } - - /** - * @throws TemporalSerializerException - */ - protected function deserializeItem(?array $value, string $className): ?TemporalSerializable - { - if (! class_exists($className)) { - throw TemporalSerializerException::targetClassDoesntExists($className); - } - - if (! in_array(TemporalSerializable::class, rescue(fn () => \Safe\class_implements($className), [], false))) { - throw TemporalSerializerException::targetClassIsNotSerializable($className); - } - - if ($value === null) { - return null; - } - - /** @var class-string $className */ - return $className::fromTemporalPayload($value); - } -} diff --git a/src/Integrations/LaravelData/TemporalSerializableCastAndTransformer.php b/src/Integrations/LaravelData/TemporalSerializableCastAndTransformer.php index b9d9b29..e689334 100644 --- a/src/Integrations/LaravelData/TemporalSerializableCastAndTransformer.php +++ b/src/Integrations/LaravelData/TemporalSerializableCastAndTransformer.php @@ -2,58 +2,51 @@ namespace Keepsuit\LaravelTemporal\Integrations\LaravelData; -use Illuminate\Container\Container; +use Keepsuit\LaravelTemporal\Contracts\TemporalSerializable; +use Keepsuit\LaravelTemporal\Exceptions\TemporalSerializerException; use Spatie\LaravelData\Casts\Cast; +use Spatie\LaravelData\Casts\IterableItemCast; +use Spatie\LaravelData\Casts\Uncastable; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataProperty; use Spatie\LaravelData\Support\Transformation\TransformationContext; use Spatie\LaravelData\Transformers\Transformer; -if (LaravelDataHelpers::version() === 4) { - class TemporalSerializableCastAndTransformer implements Cast, Transformer +class TemporalSerializableCastAndTransformer implements Cast, IterableItemCast, Transformer +{ + public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): TemporalSerializable|Uncastable { - public function __construct( - protected ?string $type = null - ) { - } + return $this->castValue($property->type->type->findAcceptedTypeForBaseType(TemporalSerializable::class), $value); + } - public function cast(DataProperty $property, mixed $value, array $properties, CreationContext $context): mixed - { - $serializer = Container::getInstance()->make(LaravelDataTemporalSerializer::class); - assert($serializer instanceof LaravelDataTemporalSerializer); + public function castIterableItem(DataProperty $property, mixed $value, array $properties, CreationContext $context): TemporalSerializable|Uncastable + { + return $this->castValue($property->type->iterableItemType, $value); + } - return $serializer->cast($property, $value, $this->type); + public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed + { + if ($value instanceof TemporalSerializable) { + return $value->toTemporalPayload(); } - public function transform(DataProperty $property, mixed $value, TransformationContext $context): mixed - { - $serializer = Container::getInstance()->make(LaravelDataTemporalSerializer::class); - - return $serializer->transform($value); - } + return $value; } -} else { - class TemporalSerializableCastAndTransformer implements Cast, Transformer + + /** + * @throws TemporalSerializerException + */ + protected function castValue(string $className, mixed $value): TemporalSerializable|Uncastable { - public function __construct( - protected ?string $type = null - ) { + if (! class_exists($className)) { + return Uncastable::create(); } - public function cast(DataProperty $property, mixed $value, array $context): mixed - { - $serializer = Container::getInstance()->make(LaravelDataTemporalSerializer::class); - assert($serializer instanceof LaravelDataTemporalSerializer); - - return $serializer->cast($property, $value, $this->type); + if (! is_array($value)) { + return Uncastable::create(); } - public function transform(DataProperty $property, mixed $value): mixed - { - $serializer = Container::getInstance()->make(LaravelDataTemporalSerializer::class); - assert($serializer instanceof LaravelDataTemporalSerializer); - - return $serializer->transform($value); - } + /** @var class-string $className */ + return $className::fromTemporalPayload($value); } } diff --git a/tests/Fixtures/Converter/AdvancedDataItem.php b/tests/Fixtures/Converter/AdvancedDataItem.php new file mode 100644 index 0000000..f6a2fe0 --- /dev/null +++ b/tests/Fixtures/Converter/AdvancedDataItem.php @@ -0,0 +1,18 @@ +|null + */ + public ?Collection $collection = null, + ) { + } +} diff --git a/tests/Fixtures/Converter/AdvancedDataItemV3.php b/tests/Fixtures/Converter/AdvancedDataItemV3.php deleted file mode 100644 index 26abb05..0000000 --- a/tests/Fixtures/Converter/AdvancedDataItemV3.php +++ /dev/null @@ -1,24 +0,0 @@ -|null - */ - #[WithCastAndTransformer(TemporalSerializableCastAndTransformer::class, type: TemporalSerializableItem::class)] - public ?Collection $collection = null, - ) { - } -} diff --git a/tests/Fixtures/Converter/DataItemV4.php b/tests/Fixtures/Converter/DataItem.php similarity index 79% rename from tests/Fixtures/Converter/DataItemV4.php rename to tests/Fixtures/Converter/DataItem.php index ed4edf4..e62da94 100644 --- a/tests/Fixtures/Converter/DataItemV4.php +++ b/tests/Fixtures/Converter/DataItem.php @@ -5,13 +5,13 @@ use Illuminate\Support\Collection; use Spatie\LaravelData\Data; -class DataItemV4 extends Data +class DataItem extends Data { public function __construct( public int $id, public ?array $values = null, /** - * @var Collection|null + * @var Collection|null */ public ?Collection $collection = null, ) { diff --git a/tests/Fixtures/Converter/DataItemV3.php b/tests/Fixtures/Converter/DataItemV3.php deleted file mode 100644 index d9ea0c3..0000000 --- a/tests/Fixtures/Converter/DataItemV3.php +++ /dev/null @@ -1,19 +0,0 @@ -|null - */ - public ?DataCollection $collection = null, - ) { - } -} diff --git a/tests/Integrations/LaravelData/LaravelDataIntegrationTest.php b/tests/Integrations/LaravelData/LaravelDataIntegrationTest.php index 0fc92b4..21b64e8 100644 --- a/tests/Integrations/LaravelData/LaravelDataIntegrationTest.php +++ b/tests/Integrations/LaravelData/LaravelDataIntegrationTest.php @@ -2,124 +2,98 @@ use Illuminate\Support\Collection; use Keepsuit\LaravelTemporal\DataConverter\LaravelPayloadConverter; -use Keepsuit\LaravelTemporal\Integrations\LaravelData\LaravelDataHelpers; -use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\AdvancedDataItemV3; -use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\AdvancedDataItemV4; -use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\DataItemV3; -use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\DataItemV4; +use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\AdvancedDataItem; +use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\DataItem; use Keepsuit\LaravelTemporal\Tests\Fixtures\Converter\TemporalSerializableItem; use Temporal\DataConverter\Type; it('can deserialize Data values', function () { $converter = new LaravelPayloadConverter(); - $payload = $converter->toPayload(new DataItemV3(123)); + $payload = $converter->toPayload(new DataItem(123)); - $data = $converter->fromPayload($payload, new Type(DataItemV3::class)); + $data = $converter->fromPayload($payload, new Type(DataItem::class)); expect($data) - ->toBeInstanceOf(DataItemV3::class) + ->toBeInstanceOf(DataItem::class) ->id->toBe(123); }); it('can convert TemporalSerializable property with cast/transformer', function () { $converter = new LaravelPayloadConverter(); - $input = new AdvancedDataItemV3(new TemporalSerializableItem(123)); + $input = new AdvancedDataItem(new TemporalSerializableItem(123)); $payload = $converter->toPayload($input); - $data = $converter->fromPayload($payload, new Type(AdvancedDataItemV3::class)); + $data = $converter->fromPayload($payload, new Type(AdvancedDataItem::class)); expect($data) - ->toBeInstanceOf(AdvancedDataItemV3::class) + ->toBeInstanceOf(AdvancedDataItem::class) ->item->id->toEqual(123); }); it('can convert TemporalSerializable array property with cast/transformer', function () { $converter = new LaravelPayloadConverter(); - $input = new AdvancedDataItemV3( + $input = new AdvancedDataItem( new TemporalSerializableItem(123), - [new TemporalSerializableItem(4), new TemporalSerializableItem(5)] + Collection::make([new TemporalSerializableItem(4), new TemporalSerializableItem(5)]) ); $payload = $converter->toPayload($input); - $data = $converter->fromPayload($payload, new Type(AdvancedDataItemV3::class)); + $data = $converter->fromPayload($payload, new Type(AdvancedDataItem::class)); expect($data) - ->toBeInstanceOf(AdvancedDataItemV3::class) + ->toBeInstanceOf(AdvancedDataItem::class) ->item->toBeInstanceOf(TemporalSerializableItem::class) ->item->id->toEqual(123) - ->collection->toBeArray() - ->collection->toHaveCount(2) - ->collection->{0}->toBeInstanceOf(TemporalSerializableItem::class) - ->collection->{0}->id->toEqual(4); -}); - -it('(v3) can deserialize Data values with data collection', function () { - $converter = new LaravelPayloadConverter(); - - $payload = $converter->toPayload(new DataItemV3( - 123, - ['a' => 1, 'b' => 2], - method_exists(DataItemV3::class, 'collection') - ? DataItemV3::collection([['id' => 4], ['id' => 5]]) - : DataItemV3::collect([['id' => 4], ['id' => 5]], \Spatie\LaravelData\DataCollection::class) - )); - - $data = $converter->fromPayload($payload, new Type(DataItemV3::class)); - - expect($data) - ->toBeInstanceOf(DataItemV3::class) - ->id->toBe(123) - ->values->toEqual(['a' => 1, 'b' => 2]) - ->collection->toBeInstanceOf(\Spatie\LaravelData\DataCollection::class) + ->collection->toBeInstanceOf(Collection::class) ->collection->toHaveCount(2) - ->collection->toCollection()->get(0)->toBeInstanceOf(DataItemV3::class) - ->collection->toCollection()->get(0)->id->toBe(4); + ->collection->first()->toBeInstanceOf(TemporalSerializableItem::class) + ->collection->first()->id->toEqual(4); }); -it('(v4) can deserialize Data values with collection', function () { +it('can deserialize Data values with collection', function () { $converter = new LaravelPayloadConverter(); - $payload = $converter->toPayload(new DataItemV4( + $payload = $converter->toPayload(new DataItem( 123, ['a' => 1, 'b' => 2], - new Collection([new DataItemV4(4), new DataItemV4(5)]) + new Collection([new DataItem(4), new DataItem(5)]) )); - $data = $converter->fromPayload($payload, new Type(DataItemV4::class)); + $data = $converter->fromPayload($payload, new Type(DataItem::class)); expect($data) - ->toBeInstanceOf(DataItemV4::class) + ->toBeInstanceOf(DataItem::class) ->id->toBe(123) ->values->toEqual(['a' => 1, 'b' => 2]) ->collection->toBeInstanceOf(Collection::class) ->collection->toHaveCount(2) - ->collection->get(0)->toBeInstanceOf(DataItemV4::class) + ->collection->get(0)->toBeInstanceOf(DataItem::class) ->collection->get(0)->id->toBe(4); -})->skip(LaravelDataHelpers::version() !== 4); +}); -it('(v4) can convert collection of TemporalSerializable', function () { +it('can convert collection of TemporalSerializable', function () { $converter = new LaravelPayloadConverter(); - $input = new AdvancedDataItemV4( + $input = new AdvancedDataItem( new TemporalSerializableItem(123), new Collection([new TemporalSerializableItem(4), new TemporalSerializableItem(5)]) ); $payload = $converter->toPayload($input); - $data = $converter->fromPayload($payload, new Type(AdvancedDataItemV4::class)); + $data = $converter->fromPayload($payload, new Type(AdvancedDataItem::class)); expect($data) - ->toBeInstanceOf(AdvancedDataItemV4::class) + ->toBeInstanceOf(AdvancedDataItem::class) ->item->toBeInstanceOf(TemporalSerializableItem::class) ->item->id->toEqual(123) ->collection->toBeInstanceOf(Collection::class) ->collection->toHaveCount(2) ->collection->get(0)->toBeInstanceOf(TemporalSerializableItem::class) ->collection->get(0)->id->toEqual(4); -})->skip(LaravelDataHelpers::version() !== 4); +}); diff --git a/tests/Integrations/LaravelData/LaravelDataTestCase.php b/tests/Integrations/LaravelData/LaravelDataTestCase.php index 575bc9f..85a1c0f 100644 --- a/tests/Integrations/LaravelData/LaravelDataTestCase.php +++ b/tests/Integrations/LaravelData/LaravelDataTestCase.php @@ -2,6 +2,9 @@ namespace Keepsuit\LaravelTemporal\Tests\Integrations\LaravelData; +use Illuminate\Config\Repository; +use Keepsuit\LaravelTemporal\Contracts\TemporalSerializable; +use Keepsuit\LaravelTemporal\Integrations\LaravelData\TemporalSerializableCastAndTransformer; use Keepsuit\LaravelTemporal\Tests\TestCase; use Spatie\LaravelData\LaravelDataServiceProvider; @@ -13,4 +16,21 @@ protected function getPackageProviders($app): array LaravelDataServiceProvider::class, ], parent::getPackageProviders($app)); } + + public function defineEnvironment($app): void + { + parent::defineEnvironment($app); + + tap($app['config'], function (Repository $config) { + $config->set('data.features.cast_and_transform_iterables', true); + $config->set('data.casts', [ + ...$config->get('data.casts', []), + TemporalSerializable::class => TemporalSerializableCastAndTransformer::class, + ]); + $config->set('data.transformers', [ + ...$config->get('data.transformers', []), + TemporalSerializable::class => TemporalSerializableCastAndTransformer::class, + ]); + }); + } }