Skip to content

Commit

Permalink
Permanent includes
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenvanassche committed Jan 23, 2024
1 parent 1b7ba58 commit 1d5d767
Show file tree
Hide file tree
Showing 10 changed files with 320 additions and 50 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ All notable changes to `laravel-data` will be documented in this file.
- Added contexts to the creation and transformation process
- Allow creating a data object or collection using a factory
- Speed up the process of creating and transforming data objects
- Add support for BNF syntax
- Rewritten docs

**Some more "internal" changes**

- Restructured tests for the future we have ahead
- The Type system was completely rewritten, allowing for a better performance and more flexibility in the future
- Benchmarks added to make data even faster

## 3.11.0 - 2023-12-21
Expand Down
6 changes: 3 additions & 3 deletions docs/as-a-data-transfer-object/collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,9 @@ SongData::collect(Song::all(), DataCollection::class)->first(); // SongData obje
In previous versions of the package it was possible to use the `collection` method to create a collection of data objects:

```php
SongData::collection(Song::all()); // returns a DataCollection of SongData objects
SongData::collection(Song::paginate()); // returns a PaginatedDataCollection of SongData objects
SongData::collection(Song::cursorPaginate()); // returns a CursorPaginatedCollection of SongData objects
SongData::collect(Song::all()); // returns a DataCollection of SongData objects
SongData::collect(Song::paginate()); // returns a PaginatedDataCollection of SongData objects
SongData::collect(Song::cursorPaginate()); // returns a CursorPaginatedCollection of SongData objects
```

This method was removed with version v4 of the package in favor for the more powerful `collect` method. The `collection` method can still be used by using the `WithDeprecatedCollectionMethod` trait:
Expand Down
32 changes: 28 additions & 4 deletions docs/as-a-data-transfer-object/creating-a-data-object.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Creating a data object
title: Creating a data object
weight: 1
---

Expand Down Expand Up @@ -52,7 +52,8 @@ Data can also be created from JSON strings:
SongData::from('{"title" : "Never Gonna Give You Up","artist" : "Rick Astley"}');
```

Although the PHP 8.0 constructor properties look great in data objects, it is perfectly valid to use regular properties without a constructor like so:
Although the PHP 8.0 constructor properties look great in data objects, it is perfectly valid to use regular properties
without a constructor like so:

```php
class SongData extends Data
Expand Down Expand Up @@ -160,7 +161,8 @@ will try to create itself from the following types:
- An *Arrayable* by calling `toArray` on it
- An *array*

This list can be extended using extra normalizers, find more about it [here](https://spatie.be/docs/laravel-data/v4/advanced-usage/normalizers).
This list can be extended using extra normalizers, find more about
it [here](https://spatie.be/docs/laravel-data/v4/advanced-usage/normalizers).

When a data object cannot be created using magical methods or the default methods, a `CannotCreateData`
exception will be thrown.
Expand All @@ -187,4 +189,26 @@ SongData::withoutMagicalCreationFrom($song);

## Advanced creation using factories

It is possible to configure how a data object is created, whether it will be validated, which casts to use and more. You can read more about it [here](/docs/laravel-data/v4/advanced-usage/factories).
It is possible to configure how a data object is created, whether it will be validated, which casts to use and more. You
can read more about it [here](/docs/laravel-data/v4/advanced-usage/factories).

## DTO classes

The default `Data` class from which you extend your data objects is a multi versatile class, it packs a lot of
functionality. But sometimes you just want a simple DTO class. You can use the `Dto` class for this:

```php
class SongData extends Dto
{
public function __construct(
public string $title,
public string $artist,
) {
}
}
```

The `Dto` class is a data class in its most basic form. It can br created from anything using magical methods, can
validate payloads before creating the data object and can be created using factories. But it doesn't have any of the
other functionality that the `Data` class has.

20 changes: 19 additions & 1 deletion docs/as-a-resource/from-data-to-resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,25 @@ SongData::empty([
]);
```

## Response status code
## Response status code

When a resource is being returned from a controller, the status code of the response will automatically be set to `201 CREATED` when Laravel data detects that the request's method is `POST`. In all other cases, `200 OK` will be returned.

## Resource classes

To make it a bit more clear that a data object is a resource, you can use the `Resource` class instead of the `Data` class:

```php
use Spatie\LaravelData\Resource;

class SongResource extends Resource
{
public function __construct(
public string $title,
public string $artist,
) {
}
}
```

These resource classes have as an advantage that they won't validate data or check authorization, They are only used to transform data which makes them a bit faster.
97 changes: 77 additions & 20 deletions docs/as-a-resource/lazy-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ Sometimes you don't want all the properties included when transforming a data ob
```php
class AlbumData extends Data
{
/**
* @param Collection<int, SongData> $songs
*/
public function __construct(
public string $title,
#[DataCollectionOf(SongData::class)]
public DataCollection $songs,
public Collection $songs,
) {
}
}
Expand All @@ -22,18 +24,20 @@ This will always output a collection of songs, which can become quite large. Wit
```php
class AlbumData extends Data
{
/**
* @param Lazy|Collection<int, SongData> $songs
*/
public function __construct(
public string $title,
#[DataCollectionOf(SongData::class)]
public Lazy|DataCollection $songs,
public Lazy|Collection $songs,
) {
}

public static function fromModel(Album $album): self
{
return new self(
$album->title,
Lazy::create(fn() => SongData::collection($album->songs))
Lazy::create(fn() => SongData::collect($album->songs))
);
}
}
Expand All @@ -47,23 +51,23 @@ Now when we transform the data object as such:
AlbumData::from(Album::first())->toArray();
```

We get the following JSON:
We get the following array:

```json
{
"name": "Together Forever"
}
```php
[
'title' => 'Together Forever',
]
```

As you can see, the `songs` property is missing in the JSON output. Here's how you can include it.
As you can see, the `songs` property is missing in the array output. Here's how you can include it.

```php
AlbumData::from(Album::first())->include('songs');
```

## Including lazy properties

Properties will only be included when the `include` method is called on the data object with the property's name.
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:

Expand Down Expand Up @@ -138,15 +142,15 @@ return UserData::from(Auth::user())->include('favorite_song.name');
You can include lazy properties in different ways:

```php
Lazy::create(fn() => SongData::collection($album->songs));
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:

```php
Lazy::when(fn() => $this->is_admin, fn() => SongData::collection($album->songs));
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.
Expand All @@ -156,7 +160,7 @@ The property will only be included when the `is_admin` property of the data obje
You can also only include a lazy property when a particular relation is loaded on the model as such:

```php
Lazy::whenLoaded('songs', $album, fn() => SongData::collection($album->songs));
Lazy::whenLoaded('songs', $album, fn() => SongData::collect($album->songs));
```

Now the property will only be included when the song's relation is loaded on the model.
Expand All @@ -166,7 +170,7 @@ Now the property will only be included when the song's relation is loaded on the
It is possible to mark a lazy property as included by default:

```php
Lazy::create(fn() => SongData::collection($album->songs))->defaultIncluded();
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:
Expand Down Expand Up @@ -225,10 +229,12 @@ In some cases you may want to define an include on a class level by implementing
```php
class AlbumData extends Data
{
/**
* @param Lazy|Collection<SongData> $songs
*/
public function __construct(
public string $title,
#[DataCollectionOf(SongData::class)]
public Lazy|DataCollection $songs,
public Lazy|Collection $songs,
) {
}

Expand All @@ -246,10 +252,12 @@ It is even possible to include nested properties:
```php
class AlbumData extends Data
{
/**
* @param Lazy|Collection<SongData> $songs
*/
public function __construct(
public string $title,
#[DataCollectionOf(SongData::class)]
public Lazy|DataCollection $songs,
public Lazy|Collection $songs,
) {
}

Expand Down Expand Up @@ -378,3 +386,52 @@ It is also possible to run exclude, except and only operations on a data object:
- You can define **except** in `allowedRequestExcept` and use the `except` key in your query string
- You can define **only** in `allowedRequestOnly` and use the `only` key in your query string

## Mutability

Adding includes/excludes/only/except to a data object will only affect the data object (and its nested chain) once:

```php
AlbumData::from(Album::first())->include('songs')->toArray(); // will include songs
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:

```php
class AlbumData extends Data
{
/**
* @param Lazy|Collection<SongData> $songs
*/
public function __construct(
public string $title,
public Lazy|Collection $songs,
) {
}

public function includeProperties(): array
{
return [
'songs'
];
}
}
```

Or use the permanent methods:

```php
AlbumData::from(Album::first())->includePermanently('songs');
AlbumData::from(Album::first())->excludePermanently('songs');
AlbumData::from(Album::first())->onlyPermanently('songs');
AlbumData::from(Album::first())->exceptPermanently('songs');
```

When using conditional a includes/excludes/only/except, you can set the permanent flag:

```php
AlbumData::from(Album::first())->includeWhen('songs', fn(AlbumData $data) => count($data->songs) > 0, permanent: true);
AlbumData::from(Album::first())->excludeWhen('songs', fn(AlbumData $data) => count($data->songs) > 0, permanent: true);
AlbumData::from(Album::first())->onlyWhen('songs', fn(AlbumData $data) => count($data->songs) > 0), permanent: true);
AlbumData::from(Album::first())->except('songs', fn(AlbumData $data) => count($data->songs) > 0, permanent: true);
```
67 changes: 58 additions & 9 deletions docs/as-a-resource/transformers.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ title: Transforming data
weight: 7
---

Each property of a data object should be transformed into a usable type to communicate via JSON.
Transformers allow you to transform complex types to simple types. This is useful when you want to transform a data object to an array or JSON.

No complex transformations are required for the default types (string, bool, int, float, enum and array), but special types like `Carbon` or a Laravel Model will need extra attention.

Transformers are simple classes that will convert a complex type to something simple like a `string` or `int`. For example, we can transform a `Carbon` object to `16-05-1994`, `16-05-1994T00:00:00+00` or something completely different.
Transformers are simple classes that will convert a such complex types to something simple like a `string` or `int`. For example, we can transform a `Carbon` object to `16-05-1994`, `16-05-1994T00:00:00+00` or something completely different.

There are two ways you can define transformers: locally and globally.

Expand Down Expand Up @@ -80,16 +80,65 @@ ArtistData::from($artist)->all();

## Getting a data object (on steroids)

Internally the package uses the `transform` method for operations like `toArray`, `all`, `toJson` and so on. This method is highly configurable:
Internally the package uses the `transform` method for operations like `toArray`, `all`, `toJson` and so on. This method is highly configurable, when calling it without any arguments it will behave like the `toArray` method:

```php
ArtistData::from($artist)->transform();
```

Producing the following result:

```php
[
'name' => 'Rick Astley',
'birth_date' => '06-02-1966',
]
```

It is possible to disable the transformation of values, which will make the `transform` method behave like the `all` method:

```php
use Spatie\LaravelData\Support\Transformation\TransformationContext;

ArtistData::from($artist)->transform(
bool $transformValues = true,
WrapExecutionType $wrapExecutionType = WrapExecutionType::Disabled,
bool $mapPropertyNames = true,
TransformationContext::create()->withoutTransformingValues()
);
```

- **$transformValues** when enabled transformers will be used to transform properties, also data objects and collections will be transformed
- **$wrapExecutionType** allows you to set if wrapping is `Enabled` or `Disabled`
- **$mapPropertyNames** uses defined mappers to rename properties when enabled
Outputting the following array:

```php
[
'name' => 'Rick Astley',
'birth_date' => Carbon::parse('06-02-1966'),
]
```

The [mapping of property names](/docs/laravel-data/v4/as-a-resource/mapping-property-names) can also be disabled:

```php
ArtistData::from($artist)->transform(
TransformationContext::create()->mapPropertyNames(false)
);
```

It is possible to enable [wrapping](/docs/laravel-data/v4/as-a-resource/wrapping-data) the data object:

```php
use Spatie\LaravelData\Support\Wrapping\WrapExecutionType;

ArtistData::from($artist)->transform(
TransformationContext::create()->wrapExecutionType(WrapExecutionType::Enabled)
);
```

Outputting the following array:

```php
[
'data' => [
'name' => 'Rick Astley',
'birth_date' => '06-02-1966',
],
]
```
Loading

0 comments on commit 1d5d767

Please sign in to comment.