diff --git a/docs/advanced-usage/use-with-inertia.md b/docs/advanced-usage/use-with-inertia.md index 62ca8a12..8fc77f64 100644 --- a/docs/advanced-usage/use-with-inertia.md +++ b/docs/advanced-usage/use-with-inertia.md @@ -52,3 +52,38 @@ router.reload((url, { only: ['title'], }); ``` + +### Auto lazy Inertia properties + +We already saw earlier that the package can automatically make properties Lazy, the same can be done for Inertia properties. + +It is possible to rewrite the previous example as follows: + +```php +use Spatie\LaravelData\Attributes\AutoClosureLazy;use Spatie\LaravelData\Attributes\AutoInertiaLazy; + +class SongData extends Data +{ + public function __construct( + #[AutoInertiaLazy] + public Lazy|string $title, + #[AutoClosureLazy] + public Lazy|string $artist, + ) { + } +} +``` + +If all the properties of a class should be either Inertia or closure lazy, you can use the attributes on the class level: + +```php +#[AutoInertiaLazy] +class SongData extends Data +{ + public function __construct( + public Lazy|string $title, + public Lazy|string $artist, + ) { + } +} +``` diff --git a/docs/as-a-resource/lazy-properties.md b/docs/as-a-resource/lazy-properties.md index bf5087ea..e02416b2 100644 --- a/docs/as-a-resource/lazy-properties.md +++ b/docs/as-a-resource/lazy-properties.md @@ -19,7 +19,8 @@ class AlbumData extends Data } ``` -This will always output a collection of songs, which can become quite large. With lazy properties, we can include properties when we want to: +This will always output a collection of songs, which can become quite large. With lazy properties, we can include +properties when we want to: ```php class AlbumData extends Data @@ -43,7 +44,8 @@ class AlbumData extends Data } ``` -The `songs` key won't be included in the resource when transforming it from a model. Because the closure that provides the data won't be called when transforming the data object unless we explicitly demand it. +The `songs` key won't be included in the resource when transforming it from a model. Because the closure that provides +the data won't be called when transforming the data object unless we explicitly demand it. Now when we transform the data object as such: @@ -69,7 +71,8 @@ AlbumData::from(Album::first())->include('songs'); 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: +It is also possible to nest these includes. For example, let's update the `SongData` class and make all of its +properties lazy: ```php class SongData extends Data @@ -108,7 +111,8 @@ If you want to include all the properties of a data object, you can do the follo AlbumData::from(Album::first())->include('songs.*'); ``` -Explicitly including properties of data objects also works on a single data object. For example, our `UserData` looks like this: +Explicitly including properties of data objects also works on a single data object. For example, our `UserData` looks +like this: ```php class UserData extends Data @@ -147,13 +151,15 @@ Lazy::create(fn() => SongData::collect($album->songs)); With a basic `Lazy` property, you must explicitly include it when the data object is transformed. -Sometimes you only want to include a property when a specific condition is true. This can be done with conditional lazy properties: +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::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. +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. ### Relational Lazy properties @@ -173,15 +179,138 @@ It is possible to mark a lazy property as included by default: 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: +The property will now always be included when the data object is transformed. You can explicitly exclude properties that +were default included as such: ```php AlbumData::create(Album::first())->exclude('songs'); ``` +## Auto Lazy + +Writing Lazy properties can be a bit cumbersome. It is often a repetitive task to write the same code over and over +again while the package can infer almost everything. + +Let's take a look at our previous example: + +```php +class UserData extends Data +{ + public function __construct( + public string $title, + public Lazy|SongData $favorite_song, + ) { + } + + public static function fromModel(User $user): self + { + return new self( + $user->title, + Lazy::create(fn() => SongData::from($user->favorite_song)) + ); + } +} +``` + +The package knows how to get the property from the model and wrap it into a data object, but since we're using a lazy +property, we need to write our own magic creation method with a lot of repetitive code. + +In such a situation auto lazy might be a good fit, instead of casting the property directly into the data object, the +casting process is wrapped in a lazy Closure. + +This makes it possible to rewrite the example as such: + +```php +#[AutoLazy] +class UserData extends Data +{ + public function __construct( + public string $title, + public Lazy|SongData $favorite_song, + ) { + } +} +``` + +While achieving the same result! + +Auto Lazy wraps the casting process of a value for every property typed as `Lazy` into a Lazy Closure when the +`AutoLazy` attribute is present on the class. + +It is also possible to use the `AutoLazy` attribute on a property level: + +```php +class UserData extends Data +{ + public function __construct( + public string $title, + #[AutoLazy] + public Lazy|SongData $favorite_song, + ) { + } +} +``` + +The auto lazy process won't be applied in the following situations: + +- When a null value is passed to the property +- When the property value isn't present in the input payload and the property typed as `Optional` +- When a Lazy Closure is passed to the property + +### Auto lazy with model relations + +When you're constructing a data object from an Eloquent model, it is also possible to automatically create lazy +properties for model relations which are only resolved when the relation is loaded: + +```php +class UserData extends Data +{ + public function __construct( + public string $title, + #[AutoWhenLoadedLazy] + public Lazy|SongData $favoriteSong, + ) { + } +} +``` + +When the `favoriteSong` relation is loaded on the model, the property will be included in the data object. + +If the name of the relation doesn't match the property name, you can specify the relation name: + +```php +class UserData extends Data +{ + public function __construct( + public string $title, + #[AutoWhenLoadedLazy('favoriteSong')] + public Lazy|SongData $favorite_song, + ) { + } +} +``` + +The package will use the regular casting process when the relation is loaded, so it is also perfectly possible to create a collection of data objects: + +```php +class UserData extends Data +{ + /** + * @param Lazy|array $favoriteSongs + */ + public function __construct( + public string $title, + #[AutoWhenLoadedLazy] + public Lazy|array $favoriteSongs, + ) { + } +} +``` + ## Only and Except -Lazy properties are great for reducing payloads sent over the wire. However, when you completely want to remove a property Laravel's `only` and `except` methods can be used: +Lazy properties are great for reducing payloads sent over the wire. However, when you completely want to remove a +property Laravel's `only` and `except` methods can be used: ```php AlbumData::from(Album::first())->only('songs'); // will only show `songs` @@ -202,7 +331,8 @@ AlbumData::from(Album::first())->only('songs.{name, artist}'); AlbumData::from(Album::first())->except('songs.{name, artist}'); ``` -Only and except always take precedence over include and exclude, which means that when a property is hidden by `only` or `except` it is impossible to show it again using `include`. +Only and except always take precedence over include and exclude, which means that when a property is hidden by `only` or +`except` it is impossible to show it again using `include`. ### Conditionally @@ -306,7 +436,7 @@ Our JSON would look like this when we request `https://spatie.be/my-account`: ```json { - "name": "Ruben Van Assche" + "name" : "Ruben Van Assche" } ``` @@ -318,8 +448,8 @@ https://spatie.be/my-account?include=favorite_song ```json { - "name": "Ruben Van Assche", - "favorite_song": { + "name" : "Ruben Van Assche", + "favorite_song" : { "name" : "Never Gonna Give You Up", "artist" : "Rick Astley" } @@ -395,7 +525,8 @@ AlbumData::from(Album::first())->include('songs')->toArray(); // will include so 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: +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 diff --git a/src/Attributes/AutoClosureLazy.php b/src/Attributes/AutoClosureLazy.php new file mode 100644 index 00000000..6793ed85 --- /dev/null +++ b/src/Attributes/AutoClosureLazy.php @@ -0,0 +1,18 @@ + $castValue($value)); + } +} diff --git a/src/Attributes/AutoInertiaLazy.php b/src/Attributes/AutoInertiaLazy.php new file mode 100644 index 00000000..280337d8 --- /dev/null +++ b/src/Attributes/AutoInertiaLazy.php @@ -0,0 +1,18 @@ + $castValue($value)); + } +} diff --git a/src/Attributes/AutoLazy.php b/src/Attributes/AutoLazy.php new file mode 100644 index 00000000..b78c504d --- /dev/null +++ b/src/Attributes/AutoLazy.php @@ -0,0 +1,17 @@ + $castValue($value)); + } +} diff --git a/src/Attributes/AutoWhenLoadedLazy.php b/src/Attributes/AutoWhenLoadedLazy.php new file mode 100644 index 00000000..6f381ecc --- /dev/null +++ b/src/Attributes/AutoWhenLoadedLazy.php @@ -0,0 +1,27 @@ +relation ?? $property->name; + + return Lazy::when(fn () => $payload->relationLoaded($relation), fn () => $castValue( + $payload->getRelation($relation) + )); + } +} diff --git a/src/DataPipes/CastPropertiesDataPipe.php b/src/DataPipes/CastPropertiesDataPipe.php index df85cead..2c6bb206 100644 --- a/src/DataPipes/CastPropertiesDataPipe.php +++ b/src/DataPipes/CastPropertiesDataPipe.php @@ -40,7 +40,28 @@ public function handle( continue; } - $properties[$name] = $this->cast($dataProperty, $value, $properties, $creationContext); + if ($dataProperty->autoLazy) { + $properties[$name] = $dataProperty->autoLazy->build( + fn (mixed $value) => $this->cast( + $dataProperty, + $value, + $properties, + $creationContext + ), + $payload, + $dataProperty, + $value + ); + + continue; + } + + $properties[$name] = $this->cast( + $dataProperty, + $value, + $properties, + $creationContext + ); } return $properties; diff --git a/src/DataPipes/DefaultValuesDataPipe.php b/src/DataPipes/DefaultValuesDataPipe.php index fcea69e0..827349aa 100644 --- a/src/DataPipes/DefaultValuesDataPipe.php +++ b/src/DataPipes/DefaultValuesDataPipe.php @@ -2,6 +2,8 @@ namespace Spatie\LaravelData\DataPipes; +use Illuminate\Database\Eloquent\Model; +use Spatie\LaravelData\Attributes\AutoWhenLoadedLazy; use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; @@ -31,6 +33,15 @@ public function handle( continue; } + if ($property->autoLazy + && $property->autoLazy instanceof AutoWhenLoadedLazy + && $property->autoLazy->relation !== null + && $payload instanceof Model + && $payload->relationLoaded($property->autoLazy->relation) + ) { + $properties[$name] = $payload->getRelation($property->autoLazy->relation); + } + if ($property->type->isNullable) { $properties[$name] = null; } diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 1bd86c85..5075c03f 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; +use Spatie\LaravelData\Attributes\AutoLazy; use Spatie\LaravelData\Casts\Cast; use Spatie\LaravelData\Transformers\Transformer; @@ -20,6 +21,7 @@ public function __construct( public readonly bool $hidden, public readonly bool $isPromoted, public readonly bool $isReadonly, + public readonly ?AutoLazy $autoLazy, public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, public readonly ?Cast $cast, diff --git a/src/Support/Factories/DataClassFactory.php b/src/Support/Factories/DataClassFactory.php index 7082a36e..4aa89f98 100644 --- a/src/Support/Factories/DataClassFactory.php +++ b/src/Support/Factories/DataClassFactory.php @@ -8,6 +8,7 @@ use ReflectionMethod; use ReflectionParameter; use ReflectionProperty; +use Spatie\LaravelData\Attributes\AutoLazy; use Spatie\LaravelData\Contracts\AppendableData; use Spatie\LaravelData\Contracts\EmptyData; use Spatie\LaravelData\Contracts\IncludeableData; @@ -53,11 +54,16 @@ public function build(ReflectionClass $reflectionClass): DataClass ); } + $autoLazy = $attributes->first( + fn (object $attribute) => $attribute instanceof AutoLazy + ); + $properties = $this->resolveProperties( $reflectionClass, $constructorReflectionMethod, NameMappersResolver::create(ignoredMappers: [ProvidedNameMapper::class])->execute($attributes), $dataIterablePropertyAnnotations, + $autoLazy ); $responsable = $reflectionClass->implementsInterface(ResponsableData::class); @@ -135,6 +141,7 @@ protected function resolveProperties( ?ReflectionMethod $constructorReflectionMethod, array $mappers, array $dataIterablePropertyAnnotations, + ?AutoLazy $autoLazy ): Collection { $defaultValues = $this->resolveDefaultValues($reflectionClass, $constructorReflectionMethod); @@ -150,6 +157,7 @@ protected function resolveProperties( $mappers['inputNameMapper'], $mappers['outputNameMapper'], $dataIterablePropertyAnnotations[$property->getName()] ?? null, + classAutoLazy: $autoLazy ), ]); } diff --git a/src/Support/Factories/DataPropertyFactory.php b/src/Support/Factories/DataPropertyFactory.php index 1c1a519e..4b71c86b 100644 --- a/src/Support/Factories/DataPropertyFactory.php +++ b/src/Support/Factories/DataPropertyFactory.php @@ -5,6 +5,7 @@ use ReflectionAttribute; use ReflectionClass; use ReflectionProperty; +use Spatie\LaravelData\Attributes\AutoLazy; use Spatie\LaravelData\Attributes\Computed; use Spatie\LaravelData\Attributes\GetsCast; use Spatie\LaravelData\Attributes\Hidden; @@ -32,11 +33,20 @@ public function build( ?NameMapper $classInputNameMapper = null, ?NameMapper $classOutputNameMapper = null, ?DataIterableAnnotation $classDefinedDataIterableAnnotation = null, + ?AutoLazy $classAutoLazy = null, ): DataProperty { $attributes = collect($reflectionProperty->getAttributes()) ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); + $type = $this->typeFactory->buildProperty( + $reflectionProperty->getType(), + $reflectionClass, + $reflectionProperty, + $attributes, + $classDefinedDataIterableAnnotation + ); + $mappers = NameMappersResolver::create()->execute($attributes); $inputMappedName = match (true) { @@ -73,21 +83,24 @@ public function build( $defaultValue = null; } + $autoLazy = $attributes->first( + fn (object $attribute) => $attribute instanceof AutoLazy + ); + + if ($classAutoLazy && $type->lazyType !== null && $autoLazy === null) { + $autoLazy = $classAutoLazy; + } + return new DataProperty( name: $reflectionProperty->name, className: $reflectionProperty->class, - type: $this->typeFactory->buildProperty( - $reflectionProperty->getType(), - $reflectionClass, - $reflectionProperty, - $attributes, - $classDefinedDataIterableAnnotation - ), + type: $type, validate: $validate, computed: $computed, hidden: $hidden, isPromoted: $reflectionProperty->isPromoted(), isReadonly: $reflectionProperty->isReadOnly(), + autoLazy: $autoLazy, hasDefaultValue: $hasDefaultValue, defaultValue: $defaultValue, cast: $attributes->first(fn (object $attribute) => $attribute instanceof GetsCast)?->get(), diff --git a/tests/CreationTest.php b/tests/CreationTest.php index 0b74132f..2890268f 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -10,9 +10,14 @@ use Illuminate\Support\Enumerable; use Illuminate\Support\Facades\Route; use Illuminate\Validation\ValidationException; +use Inertia\LazyProp; use function Pest\Laravel\postJson; +use Spatie\LaravelData\Attributes\AutoClosureLazy; +use Spatie\LaravelData\Attributes\AutoInertiaLazy; +use Spatie\LaravelData\Attributes\AutoLazy; +use Spatie\LaravelData\Attributes\AutoWhenLoadedLazy; use Spatie\LaravelData\Attributes\Computed; use Spatie\LaravelData\Attributes\DataCollectionOf; use Spatie\LaravelData\Attributes\Validation\Min; @@ -32,6 +37,8 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\Creation\CreationContext; use Spatie\LaravelData\Support\DataClass; +use Spatie\LaravelData\Support\Lazy\ClosureLazy; +use Spatie\LaravelData\Support\Lazy\InertiaLazy; use Spatie\LaravelData\Tests\Fakes\Castables\SimpleCastable; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCast; use Spatie\LaravelData\Tests\Fakes\Casts\ConfidentialDataCollectionCast; @@ -47,8 +54,11 @@ use Spatie\LaravelData\Tests\Fakes\DataWithArgumentCountErrorException; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; +use Spatie\LaravelData\Tests\Fakes\FakeNestedModelData; use Spatie\LaravelData\Tests\Fakes\ModelData; use Spatie\LaravelData\Tests\Fakes\Models\DummyModel; +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\NestedData; use Spatie\LaravelData\Tests\Fakes\NestedLazyData; @@ -1270,3 +1280,175 @@ public static function pipeline(): DataPipeline 'year' => 2025, ]); }); + +it('can create a data object with auto lazy properties', function () { + $dataClass = new class () extends Data { + #[AutoLazy] + public Lazy|SimpleData $data; + + /** @var Lazy|Collection */ + #[AutoLazy] + public Lazy|Collection $dataCollection; + + #[AutoLazy] + public Lazy|string $string; + + #[AutoLazy] + public Lazy|string $overwrittenLazy; + + #[AutoLazy] + public Optional|Lazy|string $optionalLazy; + + #[AutoLazy] + public null|string|Lazy $nullableLazy; + }; + + $data = $dataClass::from([ + 'data' => 'Hello World', + 'dataCollection' => ['Hello', 'World'], + 'string' => 'Hello World', + 'overwrittenLazy' => Lazy::create(fn () => 'Overwritten Lazy'), + ]); + + expect($data->data)->toBeInstanceOf(Lazy::class); + expect($data->dataCollection)->toBeInstanceOf(Lazy::class); + expect($data->string)->toBeInstanceOf(Lazy::class); + expect($data->overwrittenLazy)->toBeInstanceOf(Lazy::class); + expect($data->optionalLazy)->toBeInstanceOf(Optional::class); + expect($data->nullableLazy)->toBeNull(); + + expect($data->toArray())->toBe([ + 'nullableLazy' => null, + ]); + + expect($data->include('data', 'dataCollection', 'string', 'overwrittenLazy')->toArray())->toBe([ + 'data' => ['string' => 'Hello World'], + 'dataCollection' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + 'string' => 'Hello World', + 'overwrittenLazy' => 'Overwritten Lazy', + 'nullableLazy' => null, + ]); +}); + +it('can create an auto-lazy class level attribute class', function () { + #[AutoLazy] + class TestAutoLazyClassAttributeData extends Data + { + public Lazy|SimpleData $data; + + /** @var Lazy|Collection */ + public Lazy|Collection $dataCollection; + + public Lazy|string $string; + + public Lazy|string $overwrittenLazy; + + public Optional|Lazy|string $optionalLazy; + + public null|string|Lazy $nullableLazy; + + public string $regularString; + } + + $data = TestAutoLazyClassAttributeData::from([ + 'data' => 'Hello World', + 'dataCollection' => ['Hello', 'World'], + 'string' => 'Hello World', + 'overwrittenLazy' => Lazy::create(fn () => 'Overwritten Lazy'), + 'regularString' => 'Hello World', + ]); + + expect($data->data)->toBeInstanceOf(Lazy::class); + expect($data->dataCollection)->toBeInstanceOf(Lazy::class); + expect($data->string)->toBeInstanceOf(Lazy::class); + expect($data->overwrittenLazy)->toBeInstanceOf(Lazy::class); + expect($data->optionalLazy)->toBeInstanceOf(Optional::class); + expect($data->nullableLazy)->toBeNull(); + expect($data->regularString)->toBe('Hello World'); + + expect($data->toArray())->toBe([ + 'nullableLazy' => null, + 'regularString' => 'Hello World', + ]); + expect($data->include('data', 'dataCollection', 'string', 'overwrittenLazy')->toArray())->toBe([ + 'data' => ['string' => 'Hello World'], + 'dataCollection' => [ + ['string' => 'Hello'], + ['string' => 'World'], + ], + 'string' => 'Hello World', + 'overwrittenLazy' => 'Overwritten Lazy', + 'nullableLazy' => null, + 'regularString' => 'Hello World', + ]); +}); + +it('can use auto lazy to construct an inertia lazy', function () { + $dataClass = new class () extends Data { + #[AutoInertiaLazy] + public string|Lazy $string; + }; + + $data = $dataClass::from(['string' => 'Hello World']); + + expect($data->string)->toBeInstanceOf(InertiaLazy::class); + expect($data->toArray()['string'])->toBeInstanceOf(LazyProp::class); +}); + +it('can use auto lazy to construct a closure lazy', function () { + $dataClass = new class () extends Data { + #[AutoClosureLazy] + public string|Lazy $string; + }; + + $data = $dataClass::from(['string' => 'Hello World']); + + expect($data->string)->toBeInstanceOf(ClosureLazy::class); + expect($data->toArray()['string'])->toBeInstanceOf(Closure::class); +}); + + +it('can use auto lazy to construct a when loaded lazy', function () { + $dataClass = new class () extends Data { + #[AutoWhenLoadedLazy] + /** @property array */ + public array|Lazy $fakeNestedModels; + }; + + $model = FakeModel::factory() + ->has(FakeNestedModel::factory()->count(2)) + ->create(); + + expect($dataClass::from($model)->all())->toBeEmpty(); + + $model->load('fakeNestedModels'); + + expect($dataClass::from($model)->all()['fakeNestedModels']) + ->toBeArray() + ->toHaveCount(2) + ->each()->toBeInstanceOf(FakeNestedModelData::class); +}); + +it('can use auto lazy to construct a when loaded lazy with a manual defined relation', function () { + $dataClass = new class () extends Data { + #[AutoWhenLoadedLazy('fakeNestedModels')] + /** @property array */ + public array|Lazy $models; + }; + + $model = FakeModel::factory() + ->has(FakeNestedModel::factory()->count(2)) + ->create(); + + expect($dataClass::from($model)->all())->toBeEmpty(); + + $model->load('fakeNestedModels'); + + expect($dataClass::from($model)->all()['models']) + ->toBeArray() + ->toHaveCount(2) + ->each()->toBeInstanceOf(FakeNestedModelData::class); +}); diff --git a/tests/Support/DataPropertyTest.php b/tests/Support/DataPropertyTest.php index 024a34e3..8664bb2a 100644 --- a/tests/Support/DataPropertyTest.php +++ b/tests/Support/DataPropertyTest.php @@ -1,5 +1,9 @@ build($reflectionProperty, $reflectionClass, $hasDefaultValue, $defaultValue); + return app(DataPropertyFactory::class)->build( + $reflectionProperty, + $reflectionClass, + $hasDefaultValue, + $defaultValue, + classAutoLazy: $classAutoLazy + ); } it('can get the cast attribute with arguments', function () { @@ -196,6 +208,56 @@ public function __construct( )->toBeTrue(); }); +it('can check if a property is auto-lazy', function () { + expect( + resolveHelper(new class () { + public string $property; + })->autoLazy + )->toBeNull(); + + expect( + resolveHelper(new class () { + #[AutoLazy] + public string $property; + })->autoLazy + )->toBeInstanceOf(AutoLazy::class); + + expect( + resolveHelper(new class () { + #[AutoInertiaLazy] + public string|Lazy $property; + })->autoLazy + )->toBeInstanceOf(AutoInertiaLazy::class); + + expect( + resolveHelper(new class () { + #[AutoWhenLoadedLazy('relation')] + public string|Lazy $property; + })->autoLazy + )->toBeInstanceOf(AutoWhenLoadedLazy::class); + + expect( + resolveHelper(new class () { + #[AutoClosureLazy] + public string $property; + })->autoLazy + )->toBeInstanceOf(AutoClosureLazy::class); +}); + +it('will set a property as auto-lazy when the class is auto-lazy and a lazy type is allowed', function () { + expect( + resolveHelper(new class () { + public string $property; + }, classAutoLazy: new AutoLazy())->autoLazy + )->toBeNull(); + + expect( + resolveHelper(new class () { + public string|Lazy $property; + }, classAutoLazy: new AutoLazy())->autoLazy + )->toBeInstanceOf(AutoLazy::class); +}); + it('wont throw an error if non existing attribute is used on a data class property', function () { expect(NonExistingPropertyAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') ->and(PhpStormAttributeData::from(['property' => 'hello'])->property)->toEqual('hello')