Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto lazy #831

Merged
merged 5 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/advanced-usage/use-with-inertia.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
) {
}
}
```
157 changes: 144 additions & 13 deletions docs/as-a-resource/lazy-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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<int, SongData> $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`
Expand All @@ -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

Expand Down Expand Up @@ -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"
}
```

Expand All @@ -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"
}
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions src/Attributes/AutoClosureLazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Spatie\LaravelData\Attributes;

use Attribute;
use Closure;
use Spatie\LaravelData\Lazy;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Lazy\ClosureLazy;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class AutoClosureLazy extends AutoLazy
{
public function build(Closure $castValue, mixed $payload, DataProperty $property, mixed $value): ClosureLazy
{
return Lazy::closure(fn () => $castValue($value));
}
}
18 changes: 18 additions & 0 deletions src/Attributes/AutoInertiaLazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Spatie\LaravelData\Attributes;

use Attribute;
use Closure;
use Spatie\LaravelData\Lazy;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Lazy\InertiaLazy;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class AutoInertiaLazy extends AutoLazy
{
public function build(Closure $castValue, mixed $payload, DataProperty $property, mixed $value): InertiaLazy
{
return Lazy::inertia(fn () => $castValue($value));
}
}
17 changes: 17 additions & 0 deletions src/Attributes/AutoLazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Spatie\LaravelData\Attributes;

use Attribute;
use Closure;
use Spatie\LaravelData\Lazy;
use Spatie\LaravelData\Support\DataProperty;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
class AutoLazy
{
public function build(Closure $castValue, mixed $payload, DataProperty $property, mixed $value): Lazy
{
return Lazy::create(fn () => $castValue($value));
}
}
27 changes: 27 additions & 0 deletions src/Attributes/AutoWhenLoadedLazy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Spatie\LaravelData\Attributes;

use Attribute;
use Closure;
use Spatie\LaravelData\Lazy;
use Spatie\LaravelData\Support\DataProperty;
use Spatie\LaravelData\Support\Lazy\ConditionalLazy;

#[Attribute(Attribute::TARGET_PROPERTY)]
class AutoWhenLoadedLazy extends AutoLazy
{
public function __construct(
public ?string $relation = null,
) {
}

public function build(Closure $castValue, mixed $payload, DataProperty $property, mixed $value): ConditionalLazy
{
$relation = $this->relation ?? $property->name;

return Lazy::when(fn () => $payload->relationLoaded($relation), fn () => $castValue(
$payload->getRelation($relation)
));
}
}
23 changes: 22 additions & 1 deletion src/DataPipes/CastPropertiesDataPipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading