diff --git a/UPGRADING.md b/UPGRADING.md index 93a6cac9..efb03726 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -2,6 +2,28 @@ Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. +## From v3 to v4 + +The following things are required when upgrading: + +- Start by going through your code and replace all static `SomeData::collection($items)` method calls with `SomeData::collect($items, DataCollection::class)` + - Use `DataPaginatedCollection::class` when you're expecting a paginated collection + - Use `DataCursorPaginatedCollection::class` when you're expecting a cursor paginated collection + - For a more gentle upgrade you can also use the `WithDeprecatedCollectionMethod` trait which adds the collection method again, but this trait will be removed in v5 + - If you were using `$_collectionClass`, `$_paginatedCollectionClass` or `$_cursorPaginatedCollectionClass` then take a look at the magic collect functionality on information about how to replace these +- If you were manually working with `$_includes`, ` $_excludes`, `$_only`, `$_except` or `$_wrap` these can now be found within the `$_dataContext` +- We split up some traits and interfaces, if you're manually using these on you own data implementation then take a look what has changed + - DataTrait (T) and PrepareableData (T/I) were removed + - EmptyData (T/I) and ContextableData (T/I) was added +- If you were calling the transform method on a data object, a `TransformationContextFactory` or `TransformationContext` is now the only parameter you can pass + - Take a look within the docs what has changed +- If you were using internal data structures like `DataClass` and `DataProperty` then take a look at what has been changed +- The `DataCollectableTransformer` and `DataTransformer` were replaced with their appropriate resolvers + +We advise you to take a look at the following things: +- Take a look within your data objects if `DataCollection`'s, `DataPaginatedCollection`'s and `DataCursorPaginatedCollection`'s can be replaced with regular arrays, Laravel Collections and Paginator +- Replace `DataCollectionOf` attributes with annotations, providing IDE completion and more info for static analyzers +- Replace some `extends Data` definitions with `extends Resource` or `extends Dto` for more minimal data objects ## From v2 to v3 Upgrading to laravel data shouldn't take long, we've documented all possible changes just to provide the whole context. You probably won't have to do anything: diff --git a/docs/advanced-usage/_index.md b/docs/advanced-usage/_index.md index 5303d653..12ccdad9 100644 --- a/docs/advanced-usage/_index.md +++ b/docs/advanced-usage/_index.md @@ -1,4 +1,4 @@ --- title: Advanced usage -weight: 4 +weight: 5 --- diff --git a/docs/advanced-usage/get-data-from-a-class-quickly.md b/docs/advanced-usage/get-data-from-a-class-quickly.md new file mode 100644 index 00000000..2887c2ec --- /dev/null +++ b/docs/advanced-usage/get-data-from-a-class-quickly.md @@ -0,0 +1,54 @@ +--- +title: Get data from a class quickly +weight: 15 +--- + +By adding the `WithData` trait to a Model, Request or any class that can be magically be converted to a data object, +you'll enable support for the `getData` method. This method will automatically generate a data object for the object it +is called upon. + +For example, let's retake a look at the `Song` model we saw earlier. We can add the `WithData` trait as follows: + +```php +class Song extends Model{ + use WithData; + + protected $dataClass = SongData::class; +} +``` + +Now we can quickly get the data object for the model as such: + +```php +Song::firstOrFail($id)->getData(); // A SongData object +``` + +We can do the same with a FormRequest, we don't use a property here to define the data class but use a method instead: + +```php +class SongRequest extends FormRequest +{ + use WithData; + + protected function dataClass(): string + { + return SongData::class; + } +} +``` + +Now within a controller where the request is injected, we can get the data object like this: + +```php +class SongController +{ + public function __invoke(SongRequest $request): SongData + { + $data = $request->getData(); + + $song = Song::create($data); + + return $data; + } +} +``` diff --git a/docs/advanced-usage/validation-attributes.md b/docs/advanced-usage/validation-attributes.md index 578db49e..a231e852 100644 --- a/docs/advanced-usage/validation-attributes.md +++ b/docs/advanced-usage/validation-attributes.md @@ -1,46 +1,11 @@ --- title: Validation attributes -weight: 14 +weight: 16 --- -It is possible to validate the request before a data object is constructed. This can be done by adding validation attributes to the properties of a data object like this: +These are all the validation attributes currently available in laravel-data. -```php -class SongData extends Data -{ - public function __construct( - #[Uuid()] - public string $uuid, - #[Max(15), IP, StartsWith('192.')] - public string $ip, - ) { - } -} -``` - -## Creating your validation attribute - -It is possible to create your own validation attribute by extending the `CustomValidationAttribute` class, this class has a `getRules` method that returns the rules that should be applied to the property. - -```php -#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] -class CustomRule extends CustomValidationAttribute -{ - /** - * @return array|object|string - */ - public function getRules(ValidationPath $path): array|object|string; - { - return [new CustomRule()]; - } -} -``` - -Quick note: you can only use these rules as an attribute, not as a class rule within the static `rules` method of the data class. - -## Available validation attributes - -### Accepted +## Accepted [Docs](https://laravel.com/docs/9.x/validation#rule-accepted) @@ -49,7 +14,7 @@ Quick note: you can only use these rules as an attribute, not as a class rule wi public bool $closure; ``` -### AcceptedIf +## AcceptedIf [Docs](https://laravel.com/docs/9.x/validation#rule-accepted-if) @@ -58,7 +23,7 @@ public bool $closure; public bool $closure; ``` -### ActiveUrl +## ActiveUrl [Docs](https://laravel.com/docs/9.x/validation#rule-active-url) @@ -67,7 +32,7 @@ public bool $closure; public string $closure; ``` -### After +## After [Docs](https://laravel.com/docs/9.x/validation#rule-after) @@ -83,7 +48,7 @@ public Carbon $closure; public Carbon $closure; ``` -### AfterOrEqual +## AfterOrEqual [Docs](https://laravel.com/docs/9.x/validation#rule-after-or-equal) @@ -99,7 +64,7 @@ public Carbon $closure; public Carbon $closure; ``` -### Alpha +## Alpha [Docs](https://laravel.com/docs/9.x/validation#rule-alpha) @@ -108,7 +73,7 @@ public Carbon $closure; public string $closure; ``` -### AlphaDash +## AlphaDash [Docs](https://laravel.com/docs/9.x/validation#rule-alpha-dash) @@ -117,7 +82,7 @@ public string $closure; public string $closure; ``` -### AlphaNumeric +## AlphaNumeric [Docs](https://laravel.com/docs/9.x/validation#rule-alpha-num) @@ -126,7 +91,7 @@ public string $closure; public string $closure; ``` -### ArrayType +## ArrayType [Docs](https://laravel.com/docs/9.x/validation#rule-array) @@ -141,7 +106,7 @@ public array $closure; public array $closure; ``` -### Bail +## Bail [Docs](https://laravel.com/docs/9.x/validation#rule-bail) @@ -150,7 +115,7 @@ public array $closure; public string $closure; ``` -### Before +## Before [Docs](https://laravel.com/docs/9.x/validation#rule-before) @@ -166,7 +131,7 @@ public Carbon $closure; public Carbon $closure; ``` -### BeforeOrEqual +## BeforeOrEqual [Docs](https://laravel.com/docs/9.x/validation#rule-before-or-equal) @@ -182,7 +147,7 @@ public Carbon $closure; public Carbon $closure; ``` -### Between +## Between [Docs](https://laravel.com/docs/9.x/validation#rule-between) @@ -191,7 +156,7 @@ public Carbon $closure; public int $closure; ``` -### BooleanType +## BooleanType [Docs](https://laravel.com/docs/9.x/validation#rule-boolean) @@ -200,7 +165,7 @@ public int $closure; public bool $closure; ``` -### Confirmed +## Confirmed [Docs](https://laravel.com/docs/9.x/validation#rule-confirmed) @@ -209,7 +174,7 @@ public bool $closure; public string $closure; ``` -### CurrentPassword +## CurrentPassword [Docs](https://laravel.com/docs/9.x/validation#rule-current-password) @@ -221,7 +186,7 @@ public string $closure; public string $closure; ``` -### Date +## Date [Docs](https://laravel.com/docs/9.x/validation#rule-date) @@ -230,7 +195,7 @@ public string $closure; public Carbon $closure; ``` -### DateEquals +## DateEquals [Docs](https://laravel.com/docs/9.x/validation#rule-date-equals) @@ -242,7 +207,7 @@ public Carbon $closure; public Carbon $closure; ``` -### DateFormat +## DateFormat [Docs](https://laravel.com/docs/9.x/validation#rule-date-format) @@ -251,7 +216,7 @@ public Carbon $closure; public Carbon $closure; ``` -### Different +## Different [Docs](https://laravel.com/docs/9.x/validation#rule-different) @@ -260,7 +225,7 @@ public Carbon $closure; public string $closure; ``` -### Digits +## Digits [Docs](https://laravel.com/docs/9.x/validation#rule-digits) @@ -269,7 +234,7 @@ public string $closure; public int $closure; ``` -### DigitsBetween +## DigitsBetween [Docs](https://laravel.com/docs/9.x/validation#rule-digits-between) @@ -278,7 +243,7 @@ public int $closure; public int $closure; ``` -### Dimensions +## Dimensions [Docs](https://laravel.com/docs/9.x/validation#rule-dimensions) @@ -290,7 +255,7 @@ public UploadedFile $closure; public UploadedFile $closure; ``` -### Distinct +## Distinct [Docs](https://laravel.com/docs/9.x/validation#rule-distinct) @@ -305,7 +270,7 @@ public string $closure; public string $closure; ``` -### Email +## Email [Docs](https://laravel.com/docs/9.x/validation#rule-email) @@ -323,7 +288,7 @@ public string $closure; public string $closure; ``` -### EndsWith +## EndsWith [Docs](https://laravel.com/docs/9.x/validation#rule-ends-with) @@ -338,7 +303,7 @@ public string $closure; public string $closure; ``` -### Enum +## Enum [Docs](https://laravel.com/docs/9.x/validation#rule-enum) @@ -347,7 +312,7 @@ public string $closure; public string $closure; ``` -### ExcludeIf +## ExcludeIf *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -358,7 +323,7 @@ public string $closure; public string $closure; ``` -### ExcludeUnless +## ExcludeUnless *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -369,7 +334,7 @@ public string $closure; public string $closure; ``` -### ExcludeWithout +## ExcludeWithout *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -380,7 +345,7 @@ public string $closure; public string $closure; ``` -### Exists +## Exists [Docs](https://laravel.com/docs/9.x/validation#rule-exists) @@ -401,7 +366,7 @@ public string $closure; public string $closure; ``` -### File +## File [Docs](https://laravel.com/docs/9.x/validation#rule-file) @@ -410,7 +375,7 @@ public string $closure; public UploadedFile $closure; ``` -### Filled +## Filled [Docs](https://laravel.com/docs/9.x/validation#rule-filled) @@ -419,7 +384,7 @@ public UploadedFile $closure; public string $closure; ``` -### GreaterThan +## GreaterThan [Docs](https://laravel.com/docs/9.x/validation#rule-gt) @@ -428,7 +393,7 @@ public string $closure; public int $closure; ``` -### GreaterThanOrEqualTo +## GreaterThanOrEqualTo [Docs](https://laravel.com/docs/9.x/validation#rule-gte) @@ -437,7 +402,7 @@ public int $closure; public int $closure; ``` -### Image +## Image [Docs](https://laravel.com/docs/9.x/validation#rule-image) @@ -446,7 +411,7 @@ public int $closure; public UploadedFile $closure; ``` -### In +## In [Docs](https://laravel.com/docs/9.x/validation#rule-in) @@ -458,7 +423,7 @@ public mixed $closure; public mixed $closure; ``` -### InArray +## InArray [Docs](https://laravel.com/docs/9.x/validation#rule-in-array) @@ -467,7 +432,7 @@ public mixed $closure; public string $closure; ``` -### IntegerType +## IntegerType [Docs](https://laravel.com/docs/9.x/validation#rule-integer) @@ -476,7 +441,7 @@ public string $closure; public int $closure; ``` -### IP +## IP [Docs](https://laravel.com/docs/9.x/validation#rule-ip) @@ -485,7 +450,7 @@ public int $closure; public string $closure; ``` -### IPv4 +## IPv4 [Docs](https://laravel.com/docs/9.x/validation#rule-ipv4) @@ -494,7 +459,7 @@ public string $closure; public string $closure; ``` -### IPv6 +## IPv6 [Docs](https://laravel.com/docs/9.x/validation#rule-ipv6) @@ -503,7 +468,7 @@ public string $closure; public string $closure; ``` -### Json +## Json [Docs](https://laravel.com/docs/9.x/validation#rule-json) @@ -512,7 +477,7 @@ public string $closure; public string $closure; ``` -### LessThan +## LessThan [Docs](https://laravel.com/docs/9.x/validation#rule-lt) @@ -521,7 +486,7 @@ public string $closure; public int $closure; ``` -### LessThanOrEqualTo +## LessThanOrEqualTo [Docs](https://laravel.com/docs/9.x/validation#rule-lte) @@ -530,7 +495,7 @@ public int $closure; public int $closure; ``` -### Max +## Max [Docs](https://laravel.com/docs/9.x/validation#rule-max) @@ -539,7 +504,7 @@ public int $closure; public int $closure; ``` -### MimeTypes +## MimeTypes [Docs](https://laravel.com/docs/9.x/validation#rule-mimetypes) @@ -554,7 +519,7 @@ public UploadedFile $closure; public UploadedFile $closure; ``` -### Mimes +## Mimes [Docs](https://laravel.com/docs/9.x/validation#rule-mimes) @@ -569,7 +534,7 @@ public UploadedFile $closure; public UploadedFile $closure; ``` -### Min +## Min [Docs](https://laravel.com/docs/9.x/validation#rule-min) @@ -578,7 +543,7 @@ public UploadedFile $closure; public int $closure; ``` -### MultipleOf +## MultipleOf [Docs](https://laravel.com/docs/9.x/validation#rule-multiple-of) @@ -587,7 +552,7 @@ public int $closure; public int $closure; ``` -### NotIn +## NotIn [Docs](https://laravel.com/docs/9.x/validation#rule-not-in) @@ -599,7 +564,7 @@ public mixed $closure; public mixed $closure; ``` -### NotRegex +## NotRegex [Docs](https://laravel.com/docs/9.x/validation#rule-not-regex) @@ -608,7 +573,7 @@ public mixed $closure; public string $closure; ``` -### Nullable +## Nullable [Docs](https://laravel.com/docs/9.x/validation#rule-nullable) @@ -617,7 +582,7 @@ public string $closure; public ?string $closure; ``` -### Numeric +## Numeric [Docs](https://laravel.com/docs/9.x/validation#rule-numeric) @@ -626,7 +591,7 @@ public ?string $closure; public ?string $closure; ``` -### Password +## Password [Docs](https://laravel.com/docs/9.x/validation#rule-password) @@ -635,7 +600,7 @@ public ?string $closure; public string $closure; ``` -### Present +## Present [Docs](https://laravel.com/docs/9.x/validation#rule-present) @@ -644,7 +609,7 @@ public string $closure; public string $closure; ``` -### Prohibited +## Prohibited [Docs](https://laravel.com/docs/9.x/validation#rule-prohibited) @@ -653,7 +618,7 @@ public string $closure; public ?string $closure; ``` -### ProhibitedIf +## ProhibitedIf [Docs](https://laravel.com/docs/9.x/validation#rule-prohibited-if) @@ -665,7 +630,7 @@ public ?string $closure; public ?string $closure; ``` -### ProhibitedUnless +## ProhibitedUnless [Docs](https://laravel.com/docs/9.x/validation#rule-prohibited-unless) @@ -677,7 +642,7 @@ public ?string $closure; public ?string $closure; ``` -### Prohibits +## Prohibits [Docs](https://laravel.com/docs/9.x/validation#rule-prohibits) @@ -692,7 +657,7 @@ public ?string $closure; public ?string $closure; ``` -### Regex +## Regex [Docs](https://laravel.com/docs/9.x/validation#rule-regex) @@ -701,7 +666,7 @@ public ?string $closure; public string $closure; ``` -### Required +## Required [Docs](https://laravel.com/docs/9.x/validation#rule-required) @@ -710,7 +675,7 @@ public string $closure; public string $closure; ``` -### RequiredIf +## RequiredIf [Docs](https://laravel.com/docs/9.x/validation#rule-required-if) @@ -722,7 +687,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredUnless +## RequiredUnless [Docs](https://laravel.com/docs/9.x/validation#rule-required-unless) @@ -734,7 +699,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWith +## RequiredWith [Docs](https://laravel.com/docs/9.x/validation#rule-required-with) @@ -749,7 +714,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWithAll +## RequiredWithAll [Docs](https://laravel.com/docs/9.x/validation#rule-required-with-all) @@ -764,7 +729,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWithout +## RequiredWithout [Docs](https://laravel.com/docs/9.x/validation#rule-required-without) @@ -779,7 +744,7 @@ public ?string $closure; public ?string $closure; ``` -### RequiredWithoutAll +## RequiredWithoutAll [Docs](https://laravel.com/docs/9.x/validation#rule-required-without-all) @@ -794,7 +759,7 @@ public ?string $closure; public ?string $closure; ``` -### Rule +## Rule ```php #[Rule('string|uuid')] @@ -804,7 +769,7 @@ public string $closure; public string $closure; ``` -### Same +## Same [Docs](https://laravel.com/docs/9.x/validation#rule-same) @@ -813,7 +778,7 @@ public string $closure; public string $closure; ``` -### Size +## Size [Docs](https://laravel.com/docs/9.x/validation#rule-size) @@ -822,7 +787,7 @@ public string $closure; public string $closure; ``` -### Sometimes +## Sometimes [Docs](https://laravel.com/docs/9.x/validation#validating-when-present) @@ -831,7 +796,7 @@ public string $closure; public string $closure; ``` -### StartsWith +## StartsWith [Docs](https://laravel.com/docs/9.x/validation#rule-starts-with) @@ -846,7 +811,7 @@ public string $closure; public string $closure; ``` -### StringType +## StringType [Docs](https://laravel.com/docs/9.x/validation#rule-string) @@ -855,7 +820,7 @@ public string $closure; public string $closure; ``` -### TimeZone +## TimeZone [Docs](https://laravel.com/docs/9.x/validation#rule-timezone) @@ -864,7 +829,7 @@ public string $closure; public string $closure; ``` -### Unique +## Unique [Docs](https://laravel.com/docs/9.x/validation#rule-unique) @@ -888,7 +853,7 @@ public string $closure; public string $closure; ``` -### Url +## Url [Docs](https://laravel.com/docs/9.x/validation#rule-url) @@ -897,7 +862,7 @@ public string $closure; public string $closure; ``` -### Ulid +## Ulid [Docs](https://laravel.com/docs/9.x/validation#rule-ulid) @@ -906,7 +871,7 @@ public string $closure; public string $closure; ``` -### Uuid +## Uuid [Docs](https://laravel.com/docs/9.x/validation#rule-uuid) diff --git a/docs/as-a-data-transfer-object/casts.md b/docs/as-a-data-transfer-object/casts.md index 435bb5ad..77b6dd6b 100644 --- a/docs/as-a-data-transfer-object/casts.md +++ b/docs/as-a-data-transfer-object/casts.md @@ -1,6 +1,6 @@ --- title: Casts -weight: 5 +weight: 4 --- We extend our example data object just a little bit: diff --git a/docs/as-a-data-transfer-object/collections.md b/docs/as-a-data-transfer-object/collections.md index 6ca8f683..242ed4e6 100644 --- a/docs/as-a-data-transfer-object/collections.md +++ b/docs/as-a-data-transfer-object/collections.md @@ -1,148 +1,188 @@ --- title: Collections -weight: 4 +weight: 3 --- -The package provides next to the `Data` class also a `DataCollection`, `PaginatedCollection` and `CursorPaginatedCollection` class. This collection can store a set of data objects, and we advise you to use it when storing a collection of data objects within a data object. - -For example: +It is possible to create a collection of data objects by using the `collect` method: ```php -class AlbumData extends Data -{ - public function __construct( - public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, - ) { - } -} +SongData::collect([ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], +]); // returns an array of SongData objects ``` -Using specific data collections is required for internal state management within the data object, which will become clear in the following chapters. - -## Creating `DataCollection`s +Whatever type of collection you pass in, the package will return the same type of collection with the freshly created data objects within it. As long as this type is an array, Laravel collection or paginator or a class extending from it. -There are a few different ways to create a `DataCollection`: +This opens up possibilities to create collections of Eloquent models: ```php -SongData::collection([ - ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], - ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], -]); +SongData::collect(Song::all()); // return an Eloquent collection of SongData objects ``` -If you have a collection of models, you can do the following: +Or use a paginator: ```php -SongData::collection(Song::all()); +SongData::collect(Song::paginate()); // return a LengthAwarePaginator of SongData objects + +// or + +SongData::collect(Song::cursorPaginate()); // return a CursorPaginator of SongData objects ``` -It is even possible to add a collection of data objects: +Internally the `from` method of the data class will be used to create a new data object for each item in the collection. + +When the collection already exists of data objects, the `collect` method will return the same collection: ```php -SongData::collection([ +SongData::collect([ SongData::from(['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley']), SongData::from(['title' => 'Giving Up on Love', 'artist' => 'Rick Astley']), -]); +]); // returns an array of SongData objects ``` -A `DataCollection` just works like a regular array: +The collect method also allows you to cast collections from one type into another. For example, you can pass in an `array`and get back a Laravel collection: ```php -$collection = SongData::collection([ - SongData::from(['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley']) -]); +SongData::collect($songs, Collection::class); // returns a Laravel collection of SongData objects +``` -// Count the amount of items in the collection -count($collection); +This transformation will only work with non paginator collections. -// Changing an item in the collection -$collection[0]->title = 'Giving Up on Love'; +## Magically creating collections -// Adding an item to the collection -$collection[] = SongData::from(['title' => 'Never Knew Love', 'artist' => 'Rick Astley']); +We've already seen that `from` can create data objects magically. It is also possible to create a collection of data objects magically when using `collect`. -// Removing an item from the collection -unset($collection[0]); +Let's say you've implemented a custom collection class called `SongCollection`: + +```php +class SongCollection extends Collection +{ + public function __construct( + $items = [], + public array $artists = [], + ) { + parent::__construct($items); + } +} ``` -It is even possible to loop over it with a foreach: +Since the constructor of this collection requires an extra property it cannot be created automatically. However, it is possible to define a custom collect method which can create it: ```php -foreach ($songs as $song){ - echo $song->title; +class SongData extends Data +{ + public string $title; + public string $artist; + + public static function collectArray(array $items): SongCollection + { + return new SongCollection( + parent::collect($items), + array_unique(array_map(fn(SongData $song) => $song->artist, $items)) + ); + } } ``` -## Paginated collections - -It is also possible to pass in a paginated collection: +Now when collecting an array data objects a `SongCollection` will be returned: ```php -SongData::collection(Song::paginate()); +SongData::collect([ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Living on a prayer', 'artist' => 'Bon Jovi'], +]); // returns an SongCollection of SongData objects ``` -This will return a `PaginatedDataCollection` instead of a `DataCollection`. +There are a few requirements for this to work: + +- The method must be **static and public** +- The method must **start with collect** +- The method cannot be called **collect** +- A **return type** must be defined -A cursor paginated collection can also be used: +## DataCollection's, PaginatedDataCollection's and CursorPaginatedCollection's + +The package also provides a few collection classes which can be used to create collections of data objects, it was a requirement to use these classes in the past versions of the package when nesting data objects collections in data objects. This is no longer the case and there are still valid use cases for them. + +You can create a DataCollection like this: ```php -SongData::collection(Song::cursorPaginate()); +use Spatie\LaravelData\DataCollection; + +SongData::collect(Song::all(), DataCollection::class); ``` -This will result into a `CursorPaginatedCollection` +A PaginatedDataCollection can be created like this: + +```php +use Spatie\LaravelData\PaginatedDataCollection; -## Typing data within your collections +SongData::collect(Song::paginate(), PaginatedDataCollection::class); +```` -When nesting a data collection into your data object, always type the kind of data objects that will be stored within the collection: +And a CursorPaginatedCollection can be created like this: ```php -class AlbumData extends Data +use Spatie\LaravelData\CursorPaginatedCollection; + +SongData::collect(Song::cursorPaginate(), CursorPaginatedCollection::class); +``` + +### Why using these collection classes? + +We advise you to always use arrays, Laravel collections and paginators within your data objects. But let's say you have a controller like this: + +```php +class SongController { - public function __construct( - public string $title, - #[DataCollectionOf(SongData::class)] - public DataCollection $songs, - ) { + public function index() + { + return SongData::collect(Song::all()); } } ``` -Because we typed `$songs` as `SongData`, the package automatically knows it should create `SongData` objects when creating an `AlbumData` object from an array. - -There are quite a few ways to type data collections: +In the next chapters of this documentation we'll see that is possible to include or exclude properties from the data objects like this: ```php -// Without namespace - -/** @var SongData[] */ -public DataCollection $songs; +class SongController +{ + public function index() + { + return SongData::collect(Song::all(), DataCollection::class)->include('artist'); + } +} +``` -// With namespace +This will only work when you're using a `DataCollection`, `PaginatedDataCollection` or `CursorPaginatedCollection`. -/** @var \App\Data\SongData[] */ -public DataCollection $songs; -// As an array +### DataCollections -/** @var array */ -public DataCollection $songs; +DataCollections provide some extra functionalities like: -// As a data collection +```php +// Counting the amount of items in the collection +count($collection); -/** @var \Spatie\LaravelData\DataCollection */ -public DataCollection $songs; +// Changing an item in the collection +$collection[0]->title = 'Giving Up on Love'; -// With an attribute +// Adding an item to the collection +$collection[] = SongData::from(['title' => 'Never Knew Love', 'artist' => 'Rick Astley']); -#[DataCollectionOf(SongData::class)] -public DataCollection $songs; +// Removing an item from the collection +unset($collection[0]); ``` -You're free to use one of these annotations/attributes as long as you're using one of them when adding a data collection to a data object. +It is even possible to loop over it with a foreach: -## `DataCollection` methods +```php +foreach ($songs as $song){ + echo $song->title; +} +``` The `DataCollection` class implements a few of the Laravel collection methods: @@ -159,5 +199,30 @@ The `DataCollection` class implements a few of the Laravel collection methods: You can for example get the first item within a collection like this: ```php -SongData::collection(Song::all())->first(); // SongData object +SongData::collect(Song::all(), DataCollection::class)->first(); // SongData object +``` + +### The `collection` method + +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 +``` + +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: + +```php +use Spatie\LaravelData\WithDeprecatedCollectionMethod; + +class SongData extends Data +{ + use WithDeprecatedCollectionMethod; + + // ... +} ``` + +Please note that this trait will be removed in the next major version of the package. diff --git a/docs/as-a-data-transfer-object/computed.md b/docs/as-a-data-transfer-object/computed.md index dfb49f6c..2c10f95f 100644 --- a/docs/as-a-data-transfer-object/computed.md +++ b/docs/as-a-data-transfer-object/computed.md @@ -1,6 +1,6 @@ --- title: Computed values -weight: 7 +weight: 8 --- Earlier we saw how default values can be set for a data object, the same approach can be used to set computed values, although slightly different: @@ -26,7 +26,6 @@ You can now do the following: ```php SongData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche']); -SongData::validateAndCreate(['first_name' => 'Ruben', 'last_name' => 'Van Assche']); ``` Again there are a few conditions for this approach: diff --git a/docs/as-a-data-transfer-object/creating-a-data-object.md b/docs/as-a-data-transfer-object/creating-a-data-object.md index ef226167..6bda5425 100644 --- a/docs/as-a-data-transfer-object/creating-a-data-object.md +++ b/docs/as-a-data-transfer-object/creating-a-data-object.md @@ -32,8 +32,9 @@ You can use the `from` method to create a data object from nearly anything. For model like this: ```php -class Song extends Model{ - +class Song extends Model +{ + // Your model code } ``` @@ -43,14 +44,14 @@ You can create a data object from such a model like this: SongData::from(Song::firstOrFail($id)); ``` +The package will find the required properties within the model and use them to construct the data object. + Data can also be created from JSON strings: ```php SongData::from('{"title" : "Never Gonna Give You Up","artist" : "Rick Astley"}'); ``` -The package will find the required properties within the model and use them to construct the data object. - 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 @@ -61,56 +62,6 @@ class SongData extends Data } ``` -## Mapping property names - -Sometimes the property names in the array from which you're creating a data object might be different. You can define another name for a property when it is created from array using attributes: - -```php -class ContractData extends Data -{ - public function __construct( - public string $name, - #[MapInputName('record_company')] - public string $recordCompany, - ) { - } -} -``` - -Creating the data object can now be done as such: - -```php -SongData::from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); -``` - -Changing all property names in a data object to snake_case in the data the object is created from can be done as such: - -```php -#[MapInputName(SnakeCaseMapper::class)] -class ContractData extends Data -{ - public function __construct( - public string $name, - public string $recordCompany, - ) { - } -} -``` - -You can also use the `MapName` attribute when you want to combine input (see [transforming data objects](https://spatie.be/docs/laravel-data/v3/as-a-resource/from-data-to-resource#mapping-property-names)) and output property name mapping: - -```php -#[MapName(SnakeCaseMapper::class)] -class ContractData extends Data -{ - public function __construct( - public string $name, - public string $recordCompany, - ) { - } -} -``` - ## Magical creation It is possible to overwrite or extend the behaviour of the `from` method for specific types. So you can construct a data @@ -237,55 +188,3 @@ SongData::withoutMagicalCreationFrom($song); ## Advanced creation Internally this package is using a pipeline to create a data object from something. This pipeline exists of steps which transform properties into a correct structure and it can be completely customized. You can read more about it [here](/docs/laravel-data/v3/advanced-usage/pipeline). - -## Quickly getting data from Models, Requests, ... - -By adding the `WithData` trait to a Model, Request or any class that can be magically be converted to a data object, -you'll enable support for the `getData` method. This method will automatically generate a data object for the object it -is called upon. - -For example, let's retake a look at the `Song` model we saw earlier. We can add the `WithData` trait as follows: - -```php -class Song extends Model{ - use WithData; - - protected $dataClass = SongData::class; -} -``` - -Now we can quickly get the data object for the model as such: - -```php -Song::firstOrFail($id)->getData(); // A SongData object -``` - -We can do the same with a FormRequest, we don't use a property here to define the data class but use a method instead: - -```php -class SongRequest extends FormRequest -{ - use WithData; - - protected function dataClass(): string - { - return SongData::class; - } -} -``` - -Now within a controller where the request is injected, we can get the data object like this: - -```php -class SongController -{ - public function __invoke(SongRequest $request): SongData - { - $data = $request->getData(); - - $song = Song::create($data); - - return $data; - } -} -``` diff --git a/docs/as-a-data-transfer-object/defaults.md b/docs/as-a-data-transfer-object/defaults.md index b8822a8a..a083d4bc 100644 --- a/docs/as-a-data-transfer-object/defaults.md +++ b/docs/as-a-data-transfer-object/defaults.md @@ -1,6 +1,6 @@ --- title: Default values -weight: 6 +weight: 7 --- There are a few ways to define default values for a data object. Since a data object is just a regular PHP class, you can use the constructor to set default values: diff --git a/docs/as-a-data-transfer-object/mapping-property-names.md b/docs/as-a-data-transfer-object/mapping-property-names.md new file mode 100644 index 00000000..6ba5be7b --- /dev/null +++ b/docs/as-a-data-transfer-object/mapping-property-names.md @@ -0,0 +1,52 @@ +--- +title: Mapping property names +weight: 6 +--- + +Sometimes the property names in the array from which you're creating a data object might be different. You can define another name for a property when it is created from array using attributes: + +```php +class ContractData extends Data +{ + public function __construct( + public string $name, + #[MapInputName('record_company')] + public string $recordCompany, + ) { + } +} +``` + +Creating the data object can now be done as such: + +```php +SongData::from(['name' => 'Rick Astley', 'record_company' => 'RCA Records']); +``` + +Changing all property names in a data object to snake_case in the data the object is created from can be done as such: + +```php +#[MapInputName(SnakeCaseMapper::class)] +class ContractData extends Data +{ + public function __construct( + public string $name, + public string $recordCompany, + ) { + } +} +``` + +You can also use the `MapName` attribute when you want to combine input (see [transforming data objects](https://spatie.be/docs/laravel-data/v3/as-a-resource/from-data-to-resource#mapping-property-names)) and output property name mapping: + +```php +#[MapName(SnakeCaseMapper::class)] +class ContractData extends Data +{ + public function __construct( + public string $name, + public string $recordCompany, + ) { + } +} +``` diff --git a/docs/as-a-data-transfer-object/nesting.md b/docs/as-a-data-transfer-object/nesting.md index 2800ef5c..434d9b4e 100644 --- a/docs/as-a-data-transfer-object/nesting.md +++ b/docs/as-a-data-transfer-object/nesting.md @@ -1,6 +1,6 @@ --- title: Nesting -weight: 3 +weight: 2 --- It is possible to nest multiple data objects: @@ -46,3 +46,122 @@ AlbumData::from([ ]); ``` +## Collections of data objects + +What if you want to nest a collection of data objects within a data object? + +That's perfectly possible but there's a small catch, you should always define what kind of data objects will be stored +within the collection. This is really important later on to create validation rules for data objects or partially +transforming data objects. + +There are a few different ways to define what kind of data objects will be stored within a collection. You could use an +annotation for example which has as advantage that your IDE will have better suggestions when working with the data +object. And as an extra benefit, static analyzers like PHPStan will also be able to detect errors when you're code +is using the wrong types. + +A collection of data objects defined by annotation looks like this: + +```php +/** + * @property \App\Data\SongData[] $songs + */ +class AlbumData extends Data +{ + public function __construct( + public string $title, + public array $songs, + ) { + } +} +``` + +or like this when using properties: + +```php +class AlbumData extends Data +{ + public string $title; + + /** @var \App\Data\SongData[] */ + public array $songs; +} +``` + +If you've imported the data class you can use the short notation: + +```php +use App\Data\SongData; + +class AlbumData extends Data +{ + /** @var SongData[] */ + public array $songs; +} +``` + +It is also possible to use generics: + +```php +use App\Data\SongData; + +class AlbumData extends Data +{ + /** @var array */ + public array $songs; +} +``` + +The same is true for Laravel collections, but be sure to use two generic parameters to describe the collection. One for the collection key type and one for the data object type. + +```php +use App\Data\SongData; +use \Illuminate\Support\Collection; + +class AlbumData extends Data +{ + /** @var Collection */ + public Collection $songs; +} +``` + +You can also use an attribute to define the type of data objects that will be stored within a collection: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $title, + #[DataCollectionOf(SongData::class)] + public array $songs, + ) { + } +} +``` + +This was the old way to define the type of data objects that will be stored within a collection. It is still supported, but we recommend using the annotation. + +### Creating a data object with collection + +You can create a data object with a collection of data object just like you would create a data object with a nested data object: + +```php +new AlbumData( + 'Never gonna give you up', + [ + new SongData('Never gonna give you up', 'Rick Astley'), + new SongData('Giving up on love', 'Rick Astley'), + ] +); +``` + +Or use the magical creation which will automatically create the data objects for you and also works with collections: + +```php +AlbumData::from([ + 'title' => 'Never Gonna Give You Up', + 'songs' => [ + ['title' => 'Never Gonna Give You Up', 'artist' => 'Rick Astley'], + ['title' => 'Giving Up on Love', 'artist' => 'Rick Astley'], + ] +]); +``` diff --git a/docs/as-a-data-transfer-object/optional-properties.md b/docs/as-a-data-transfer-object/optional-properties.md index 4799871a..b37beeea 100644 --- a/docs/as-a-data-transfer-object/optional-properties.md +++ b/docs/as-a-data-transfer-object/optional-properties.md @@ -1,6 +1,6 @@ --- title: Optional properties -weight: 2 +weight: 5 --- Sometimes you have a data object with properties which shouldn't always be set, for example in a partial API update where you only want to update certain fields. In this case you can make a property `Optional` as such: diff --git a/docs/as-a-data-transfer-object/request-to-data-object.md b/docs/as-a-data-transfer-object/request-to-data-object.md index 834119f2..621b3008 100644 --- a/docs/as-a-data-transfer-object/request-to-data-object.md +++ b/docs/as-a-data-transfer-object/request-to-data-object.md @@ -1,6 +1,6 @@ --- title: From a request -weight: 8 +weight: 9 --- You can create a data object by the values given in the request. @@ -32,10 +32,9 @@ You can now inject the `SongData` class in your controller. It will already be f request. ```php -class SongController{ - ... - - public function update( +class UpdateSongController +{ + public function __invoke( Song $model, SongData $data ){ @@ -46,6 +45,26 @@ class SongController{ } ``` +As an added benefit, these values will be validated before the data object is created. If the validation fails, a `ValidationException` will be thrown which will look like you've written the validation rules yourself. + +The package will also automatically validate all requests when passed to the from method: + +```php +class UpdateSongController +{ + public function __invoke( + Song $model, + SongRequest $request + ){ + $model->update(SongData::from($request)->all()); + + return redirect()->back(); + } +} +``` + +We have a complete section within these docs dedicated to validation, you can find it [here](/docs/laravel-data/v3/validation). + ## Using validation When creating a data object from a request, the package can also validate the values from the request that will be used diff --git a/docs/as-a-resource/_index.md b/docs/as-a-resource/_index.md index 8f66c1de..153109e4 100644 --- a/docs/as-a-resource/_index.md +++ b/docs/as-a-resource/_index.md @@ -1,5 +1,5 @@ --- title: As a resource -weight: 3 +weight: 4 --- diff --git a/docs/validation/_index.md b/docs/validation/_index.md new file mode 100644 index 00000000..77dfeedc --- /dev/null +++ b/docs/validation/_index.md @@ -0,0 +1,4 @@ +--- +title: Validation +weight: 3 +--- diff --git a/docs/validation/auto-rule-inferring.md b/docs/validation/auto-rule-inferring.md new file mode 100644 index 00000000..516c5fed --- /dev/null +++ b/docs/validation/auto-rule-inferring.md @@ -0,0 +1,60 @@ +--- +title: Auto rule inferring +weight: 2 +--- + +The package will automatically infer validation rules from the data object. For example, for the following data class: + +```php +class ArtistData extends Data{ + public function __construct( + public string $name, + public int $age, + public ?string $genre, + ) { + } +} +``` + +The package will generate the following validation rules: + +```php +[ + 'name' => ['required', 'string'], + 'age' => ['required', 'integer'], + 'genre' => ['nullable', 'string'], +] +``` + +All these rules are inferred by `RuleInferrers`, special classes that will look at the types of properties and will add rules based upon that. + +Rule inferrers are configured in the `data.php` config file: + +```php +/* + * Rule inferrers can be configured here. They will automatically add + * validation rules to properties of a data object based upon + * the type of the property. + */ +'rule_inferrers' => [ + Spatie\LaravelData\RuleInferrers\SometimesRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\NullableRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\RequiredRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\BuiltInTypesRuleInferrer::class, + Spatie\LaravelData\RuleInferrers\AttributesRuleInferrer::class, +], +``` + +By default, five rule inferrers are enabled: + +- **SometimesRuleInferrer** will add a `sometimes` rule when the property is optional +- **NullableRuleInferrer** will add a `nullable` rule when the property is nullable +- **RequiredRuleInferrer** will add a `required` rule when the property is not nullable +- **BuiltInTypesRuleInferrer** will add a rules which are based upon the built-in php types: + - An `int` or `float` type will add the `numeric` rule + - A `bool` type will add the `boolean` rule + - A `string` type will add the `string` rule + - A `array` type will add the `array` rule +- **AttributesRuleInferrer** will make sure that rule attributes we described above will also add their rules + +It is possible to write your rule inferrers. You can find more information [here](/docs/laravel-data/v3/advanced-usage/creating-a-rule-inferrer). diff --git a/docs/validation/introduction.md b/docs/validation/introduction.md new file mode 100644 index 00000000..99aad577 --- /dev/null +++ b/docs/validation/introduction.md @@ -0,0 +1,309 @@ +--- +title: Introduction +weight: 1 +--- + +Laravel data allows you to create data objects from all sorts of data. One of the most common ways to create a data object is from a request and the data from a request cannot always be trusted. + +That's why it is possible to validate the data before creating the data object. You can validate requests but also arrays and other structures. + +The package will try to automatically infer validation rules from the data object, so you don't have to write them yourself. For example, a `?string` property will automatically have the `nullable` and `string` rules. + +### Important notice + +Validation is probably one of the coolest features of this package, but it is also the most complex one. We'll try to make it as straightforward as possible to validate data but in the end the Laravel validator was not written to be used in this way. So there are some limitations and quirks you should be aware of. + +In some cases it might be easier to just create a custom request class with validation rules and then call `toArray` on the request to create a data object than trying to validate the data with this package. + +## When does validation happen? + +Validation will always happen BEFORE a data object is created, once a data object is created it is assumed that the data is valid. + +At the moment there isn't a way to validate data objects, so you should implement this logic yourself. We're looking into ways to make this easier in the future. + +Validation runs automatically occasionally: + +- When injecting a data object somewhere and the data object gets created from the request +- When calling the `from` method on a data object with a request + +In all other occasions validation won't run automatically. You can always validate the data manually by calling the `validate` method on a data object: + +```php +SongData::validate( + ['title' => 'Never gonna give you up'] +); // ValidationException will be thrown because 'artist' is missing +``` + +When you also want to create the object after validation was successful you can use `validateAndCreate`: + +```php +SongData::validateAndCreate( + ['title' => 'Never gonna give you up', 'artist' => 'Rick Astley'] +); // returns a SongData object +``` + +## A quick glance at the validation functionality + +We've got a lot of documentation about validation and we suggest you read it all, but if you want to get a quick glance at the validation functionality, here's a quick overview: + +### Auto rule inferring + +The package will automatically infer validation rules from the data object. For example, for the following data class: + +```php +class ArtistData extends Data{ + public function __construct( + public string $name, + public int $age, + public ?string $genre, + ) { + } +} +``` + +The package will generate the following validation rules: + +```php +[ + 'name' => ['required', 'string'], + 'age' => ['required', 'integer'], + 'genre' => ['nullable', 'string'], +] +``` + +The package follows an algorithm to infer rules from the data object, you can read more about it [here](/docs/laravel-data/v3/validation/auto-rule-inferring). + +### Validation attributes + +It is possible to add extra rules as attributes to properties of a data object: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[Max(20)] + public string $artist, + ) { + } +} +``` + +When you provide an artist with a length of more than 20 characters, the validation will fail. + +There's a complete [chapter](/docs/laravel-data/v3/validation/using-attributes) dedicated to validation attributes. + +### Manual rules + +Sometimes you want to add rules manually, this can be done as such: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => ['required', 'string'], + 'artist' => ['required', 'string'], + ]; + } +} +``` + +You can read more about manual rules in its [dedicated chapter](/docs/laravel-data/v3/validation/manual-rules). + +### Using the container + +You can resolve a data object from the container. + +```php +app(SongData::class); +``` + +We resolve a data object from the container, it's properties will already be filled by the values of the request with matching key names. +If the request contains data that is not compatible with the data object, a validation exception will be thrown. + +### Working with the validator + +We provide a few points where you can hook into the validation process. You can read more about it in the [dedicated chapter](/docs/laravel-data/v3/validation/working-with-the-validator). + +It is for example to: + +- overwrite validation messages & attributes +- overwrite the validator itself +- overwrite the redirect when validation fails +- allow to stop validation after a failure +- overwrite the error bag + +### Authorizing a request + +Just like with Laravel requests, it is possible to authorize an action for certain people only: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function authorize(): bool + { + return Auth::user()->name === 'Ruben'; + } +} +``` + +If the method returns `false`, then an `AuthorizationException` is thrown. + +## Validation of nested data objects + +When a data object is nested inside another data object, the validation rules will also be generated for that nested object. + +```php +class SingleData{ + public function __construct( + public ArtistData $artist, + public SongData $song, + ) { + } +} +``` + +The validation rules for this class will be: + +```php +[ + 'artist' => ['array'], + 'artist.name' => ['required', 'string'], + 'artist.age' => ['required', 'integer'], + 'artist.genre' => ['nullable', 'string'], + 'song' => ['array'], + 'song.title' => ['required', 'string'], + 'song.artist' => ['required', 'string'], +] +``` + +There are a few quirky things to keep in mind when working with nested data objects, you can read all about it [here](/docs/laravel-data/v3/validation/nesting-data). + +## Validation of nested data collections + +Let's say we want to create a data object like this from a request: + +```php +class AlbumData extends Data +{ + /** + * @param array $songs + */ + public function __construct( + public string $title, + public array $songs, + ) { + } +} +``` + +Since the `SongData` has its own validation rules, the package will automatically apply them when resolving validation +rules for this object. + +In this case the validation rules for `AlbumData` would look like this: + +```php +[ + 'title' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'songs.*.title' => ['required', 'string'], + 'songs.*.artist' => ['required', 'string'], +] +``` + +More info about nested data collections can be found [here](/docs/laravel-data/v3/validation/nesting-data). + +## Default values + +When you've set some default values for a data object, the validation rules will only be generated if something else than the default is provided. + +For example when we have this data object: + +```php +class SongData extends Data +{ + public function __construct( + public string $title = 'Never Gonna Give You Up', + public string $artist = 'Rick Astley', + ) { + } +} +``` + +And we try to validate the following data: + +```php +SongData::validate( + ['title' => 'Giving Up On Love'] +); +``` + +Then the validation rules will be: + +```php +[ + 'title' => ['required', 'string'], +] +``` + +## Mapping property names + +When mapping property names, the validation rules will be generated for the mapped property name: + +```php +class SongData extends Data +{ + public function __construct( + #[MapFrom('song_title')] + public string $title, + ) { + } +} +``` + +The validation rules for this class will be: + +```php +[ + 'song_title' => ['required', 'string'], +] +``` + +There's one small catch, when the validation fails the error message will be for the original property name, not the mapped property name. This is a small quirk we hope to solve as soon as possible. + +## Retrieving validation rules for a data object + +You can retrieve the validation rules a data object will generate as such: + +```php +AlbumData::getValidationRules($payload); +``` + +This will produce the following array with rules: + +```php +[ + 'title' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'songs.*.title' => ['required', 'string'], + 'songs.*.artist' => ['required', 'string'], +] +``` + +### Payload requirement + +We suggest always to provide a payload when generating validation rules. Because such a payload is used to determine which rules will be generated and which can be skipped. diff --git a/docs/validation/manual-rules.md b/docs/validation/manual-rules.md new file mode 100644 index 00000000..83d5b43f --- /dev/null +++ b/docs/validation/manual-rules.md @@ -0,0 +1,193 @@ +--- +title: Manual rules +weight: 4 +--- + +It is also possible to write rules down manually in a dedicated method on the data object. This can come in handy when you want +to construct a custom rule object which isn't possible with attributes: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => ['required', 'string'], + 'artist' => ['required', 'string'], + ]; + } +} +``` + +By overwriting a property's rules within the `rules` method, no other rules will be inferred automatically anymore for that property. + +This means that in the following example, only a `max:20` rule will be added, and not a `string` and `required` rule: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => ['max:20'], + 'artist' => ['max:20'], + ]; + } +} + +// The generated rules will look like this +[ + 'title' => ['max:20'], + 'artist' => ['max:20'], +] +``` + +As a rule of thumb always follow these rules: + +> Always use the array syntax for defining rules and not a single string which spits the rules by | characters. +> This is needed when using regexes those | can be seen as part of the regex + +## Using attributes + +It is even possible to use the validationAttribute objects within the `rules` method: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(): array + { + return [ + 'title' => [new Required(), new StringType()], + 'artist' => [new Required(), new StringType()], + ]; + } +} +``` + + +You can even add dependencies to be automatically injected: + +```php +use SongSettingsRepository; + +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(SongSettingsRepository $settings): array + { + return [ + 'title' => [new RequiredIf($settings->forUser(auth()->user())->title_required), new StringType()], + 'artist' => [new Required(), new StringType()], + ]; + } +} +``` + +## Using context + +Sometimes a bit more context is required, in such case a `ValidationContext` parameter can be injected as such: +Additionally, if you need to access the data payload, you can use `$payload` parameter: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function rules(ValidationContext $context): array + { + return [ + 'title' => ['required'], + 'artist' => Rule::requiredIf($context->fullPayload['title'] !== 'Never Gonna Give You Up'), + ]; + } +} +``` + +By default, the provided payload is the whole request payload provided to the data object. +If you want to generate rules in nested data objects then a relative payload can be more useful: + +```php +class AlbumData extends Data +{ + /** + * @param array $songs + */ + public function __construct( + public string $title, + public array $songs, + ) { + } +} + +class SongData extends Data +{ + public function __construct( + public string $title, + public ?string $artist, + ) { + } + + public static function rules(ValidationContext $context): array + { + return [ + 'title' => ['required'], + 'artist' => Rule::requiredIf($context->payload['title'] !== 'Never Gonna Give You Up'), + ]; + } +} +``` + +When providing such payload: + +```php +[ + 'title' => 'Best songs ever made', + 'songs' => [ + ['title' => 'Never Gonna Give You Up'], + ['title' => 'Heroes', 'artist' => 'David Bowie'], + ], +]; +``` + +The rules will be: + +```php +[ + 'title' => ['string', 'required'], + 'songs' => ['present', 'array'], + 'songs.*.title' => ['string', 'required'], + 'songs.*.artist' => ['string', 'nullable'], + 'songs.*' => [NestedRules(...)], +] +``` + +It is also possible to retrieve the current path in the data object chain we're generating rules for right now by calling `$context->path`. In the case of our previous example this would be `songs.0` and `songs.1`; + +Make sure the name of the parameter is `$context` in the `rules` method, otherwise no context will be injected. diff --git a/docs/validation/nesting-data.md b/docs/validation/nesting-data.md new file mode 100644 index 00000000..6f5ba152 --- /dev/null +++ b/docs/validation/nesting-data.md @@ -0,0 +1,6 @@ +--- +title: Nesting Data +weight: 6 +--- + +Work in progress diff --git a/docs/validation/skipping-validation.md b/docs/validation/skipping-validation.md new file mode 100644 index 00000000..c8bb52fa --- /dev/null +++ b/docs/validation/skipping-validation.md @@ -0,0 +1,62 @@ +--- +title: Skipping validation +weight: 7 +--- + +Sometimes you don't want properties to be automatically validated, for instance when you're manually overwriting the +rules method like this: + +```php +class SongData extends Data +{ + public function __construct( + public string $name, + ) { + } + + public static function fromRequest(Request $request): static{ + return new self("{$request->input('first_name')} {$request->input('last_name')}") + } + + public static function rules(): array + { + return [ + 'first_name' => ['required', 'string'], + 'last_name' => ['required', 'string'], + ]; + } +} +``` + +When a request is being validated, the rules will look like this: + +```php +[ + 'name' => ['required', 'string'], + 'first_name' => ['required', 'string'], + 'last_name' => ['required', 'string'], +] +``` + +We know we never want to validate the `name` property since it won't be in the request payload, this can be done as +such: + +```php +class SongData extends Data +{ + public function __construct( + #[WithoutValidation] + public string $name, + ) { + } +} +``` + +Now the validation rules will look like this: + +```php +[ + 'first_name' => ['required', 'string'], + 'last_name' => ['required', 'string'], +] +``` diff --git a/docs/validation/using-validation-attributes.md b/docs/validation/using-validation-attributes.md new file mode 100644 index 00000000..5e437227 --- /dev/null +++ b/docs/validation/using-validation-attributes.md @@ -0,0 +1,168 @@ +--- +title: Using validation attributes +weight: 3 +--- + +It is possible to add extra rules as attributes to properties of a data object: + +```php +class SongData extends Data +{ + public function __construct( + #[Uuid()] + public string $uuid, + #[Max(15), IP, StartsWith('192.')] + public string $ip, + ) { + } +} +``` + +These rules will be merged together with the rules that are inferred from the data object. + +So it is not required to add the `required` and `string` rule, these will be added automatically. The rules for the above data object will look like this: + +```php +[ + 'uuid' => ['required', 'string', 'uuid'], + 'ip' => ['required', 'string', 'max:15', 'ip', 'starts_with:192.'], +] +``` + +For each Laravel validation rule we've got a matching validation attribute, you can find a list of them [here](/docs/laravel-data/v3/advanced-usage/using-attributes). + + +## Referencing route parameters + +Sometimes you need a value within your validation attribute which is a route parameter. +Like the example below where the id should be unique ignoring the current id: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[Unique('songs', ignore: new RouteParameterReference('song'))] + public int $id, + ) { + } +} +``` + +If the parameter is a model and another property should be used, then you can do the following: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[Unique('songs', ignore: new RouteParameterReference('song', 'uuid'))] + public string $uuid, + ) { + } +} +``` + +## Referencing other fields + +It is possible to reference other fields in validation attributes: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[RequiredUnless('title', 'Never Gonna Give You Up')] + public string $artist, + ) { + } +} +``` + +These references are always relative to the current data object. So when being nested like this: + +```php +class AlbumData extends Data +{ + public function __construct( + public string $album_name, + public SongData $song, + ) { + } +} +``` + +The generated rules will look like this: + +```php +[ + 'album_name' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'song.title' => ['required', 'string'], + 'song.artist' => ['string', 'required_if:song.title,"Never Gonna Give You Up"'], +] +``` + +If you want to reference fields starting from the root data object you can do the following: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + #[RequiredUnless(new FieldReference('album', fromRoot: true), 'Whenever You Need Somebody')] + public string $artist, + ) { + } +} +``` + +The rules will now look like this: + +```php +[ + 'album_name' => ['required', 'string'], + 'songs' => ['required', 'array'], + 'song.title' => ['required', 'string'], + 'song.artist' => ['string', 'required_if:album_name,"Whenever You Need Somebody"'], +] +``` + +## Rule attribute + +One special attribute is the `Rule` attribute. With it, you can write rules just like you would when creating a custom +Laravel request: + +```php +// using an array +#[Rule(['required', 'string'])] +public string $property + +// using a string +#[Rule('required|string')] +public string $property + +// using multiple arguments +#[Rule('required', 'string')] +public string $property +``` + +## Creating your validation attribute + +It is possible to create your own validation attribute by extending the `CustomValidationAttribute` class, this class has a `getRules` method that returns the rules that should be applied to the property. + +```php +#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)] +class CustomRule extends CustomValidationAttribute +{ + /** + * @return array|object|string + */ + public function getRules(ValidationPath $path): array|object|string; + { + return [new CustomRule()]; + } +} +``` + +Quick note: you can only use these rules as an attribute, not as a class rule within the static `rules` method of the data class. diff --git a/docs/validation/working-with-the-validator.md b/docs/validation/working-with-the-validator.md new file mode 100644 index 00000000..4df37981 --- /dev/null +++ b/docs/validation/working-with-the-validator.md @@ -0,0 +1,162 @@ +--- +title: Working with the validator +weight: 5 +--- + +Sometimes a more fine-grained control over the validation is required. In such case you can hook into the validator. + +## Overwriting messages + +It is possible to overwrite the error messages that will be returned when an error fails: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function messages(): array + { + return [ + 'title.required' => 'A title is required', + 'artist.required' => 'An artist is required', + ]; + } +} +``` + +## Overwriting attributes + +In the default Laravel validation rules, you can overwrite the name of the attribute as such: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function attributes(): array + { + return [ + 'title' => 'titel', + 'artist' => 'artiest', + ]; + } +} +``` + +## Overwriting other validation functionality + +Next to overwriting the validator, attributes and messages it is also possible to overwrite the following functionality. + +The redirect when a validation failed: + +```php +class SongData extends Data +{ + // ... + + public static function redirect(): string + { + return action(HomeController::class); + } +} +``` + +Or the route which will be used to redirect after a validation failed: + +```php +class SongData extends Data +{ + // ... + + public static function redirectRoute(): string + { + return 'home'; + } +} +``` + +Whether to stop validating on the first failure: + +```php +class SongData extends Data +{ + // ... + + public static function stopOnFirstFailure(): bool + { + return true; + } +} +``` + +The name of the error bag: + +```php +class SongData extends Data +{ + // ... + + public static function errorBag(): string + { + return 'never_gonna_give_an_error_up'; + } +} +``` + +### Using dependencies in overwritten functionality + +You can also provide dependencies to be injected in the overwritten validator functionality methods like `messages` +, `attributes`, `redirect`, `redirectRoute`, `stopOnFirstFailure`, `errorBag`: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function attributes( + ValidationAttributesLanguageRepository $validationAttributesLanguageRepository + ): array + { + return [ + 'title' => $validationAttributesLanguageRepository->get('title'), + 'artist' => $validationAttributesLanguageRepository->get('artist'), + ]; + } +} +``` + +## Overwriting the validator + +Before validating the values, it is possible to plugin into the validator. This can be done as such: + +```php +class SongData extends Data +{ + public function __construct( + public string $title, + public string $artist, + ) { + } + + public static function withValidator(Validator $validator): void + { + $validator->after(function ($validator) { + $validator->errors()->add('field', 'Something is wrong with this field!'); + }); + } +} +``` + +Please note that this method will only be called on the root data object that is being validated, all the nested data objects and collections `withValidator` methods will not be called. diff --git a/src/Concerns/DeprecatedData.php b/src/Concerns/WithDeprecatedCollectionMethod.php similarity index 97% rename from src/Concerns/DeprecatedData.php rename to src/Concerns/WithDeprecatedCollectionMethod.php index 0ad3a6ac..96ef1576 100644 --- a/src/Concerns/DeprecatedData.php +++ b/src/Concerns/WithDeprecatedCollectionMethod.php @@ -11,7 +11,7 @@ use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\PaginatedDataCollection; -trait DeprecatedData +trait WithDeprecatedCollectionMethod { /** @deprecated */ public static function collection(Enumerable|array|AbstractPaginator|Paginator|AbstractCursorPaginator|CursorPaginator|DataCollection $items): DataCollection|CursorPaginatedDataCollection|PaginatedDataCollection diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index d4292f76..25b92919 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -5,7 +5,7 @@ use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Illuminate\Support\LazyCollection; -use Spatie\LaravelData\Concerns\DeprecatedData; +use Spatie\LaravelData\Concerns\WithDeprecatedCollectionMethod; use Spatie\LaravelData\Contracts\DeprecatedData as DeprecatedDataContract; use Spatie\LaravelData\CursorPaginatedDataCollection; use Spatie\LaravelData\Data; @@ -15,7 +15,7 @@ use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\LazyData; use Spatie\LaravelData\Tests\Fakes\SimpleData; - +use Spatie\LaravelData\Tests\Fakes\Collections\CustomCollection; use function Spatie\Snapshots\assertMatchesJsonSnapshot; use function Spatie\Snapshots\assertMatchesSnapshot; @@ -303,7 +303,7 @@ public static function fromSimpleData(SimpleData $simpleData): static it('can return a custom data collection when collecting data', function () { $class = new class ('') extends Data implements DeprecatedDataContract { - use DeprecatedData; + use WithDeprecatedCollectionMethod; protected static string $_collectionClass = CustomDataCollection::class; @@ -322,7 +322,7 @@ public function __construct(public string $string) it('can return a custom paginated data collection when collecting data', function () { $class = new class ('') extends Data implements DeprecatedDataContract { - use DeprecatedData; + use WithDeprecatedCollectionMethod; protected static string $_paginatedCollectionClass = CustomPaginatedDataCollection::class; @@ -409,3 +409,46 @@ function (string $operation, array $arguments, array $expected) { expect($invaded->_dataContext)->toBeNull(); }); + +it('can use a custom collection extended from collection to collect a collection of data objects', function () { + $collection = SimpleData::collect(new CustomCollection([ + ['string' => 'A'], + ['string' => 'B'], + ])); + + expect($collection)->toBeInstanceOf(CustomCollection::class); + expect($collection[0])->toBeInstanceOf(SimpleData::class); + expect($collection[1])->toBeInstanceOf(SimpleData::class); +}); + +it('can magically collect data', function () { + class TestSomeCustomCollection extends Collection + { + } + + $dataClass = new class () extends Data { + public string $string; + + public static function fromString(string $string): self + { + $s = new self(); + + $s->string = $string; + + return $s; + } + + public static function collectArray(array $items): \TestSomeCustomCollection + { + return new \TestSomeCustomCollection($items); + } + }; + + expect($dataClass::collect(['a', 'b', 'c'])) + ->toBeInstanceOf(\TestSomeCustomCollection::class) + ->all()->toEqual([ + $dataClass::from('a'), + $dataClass::from('b'), + $dataClass::from('c'), + ]); +}); diff --git a/tests/DataTest.php b/tests/DataTest.php index 8c767898..c306a2de 100644 --- a/tests/DataTest.php +++ b/tests/DataTest.php @@ -1426,42 +1426,6 @@ public function __construct( ]); }); -it('can write collection logic in a class', function () { - class TestSomeCustomCollection extends Collection - { - public function nameAll(): string - { - return $this->map(fn ($data) => $data->string)->join(', '); - } - } - - $dataClass = new class () extends Data { - public string $string; - - public static function fromString(string $string): self - { - $s = new self(); - - $s->string = $string; - - return $s; - } - - public static function collectArray(array $items): \TestSomeCustomCollection - { - return new \TestSomeCustomCollection($items); - } - }; - - expect($dataClass::collect(['a', 'b', 'c'])) - ->toBeInstanceOf(\TestSomeCustomCollection::class) - ->all()->toEqual([ - $dataClass::from('a'), - $dataClass::from('b'), - $dataClass::from('c'), - ]); -}); - it('can set a default value for data object', function () { $dataObject = new class ('', '') extends Data { diff --git a/tests/Fakes/Collections/CustomCollection.php b/tests/Fakes/Collections/CustomCollection.php new file mode 100644 index 00000000..4087b71e --- /dev/null +++ b/tests/Fakes/Collections/CustomCollection.php @@ -0,0 +1,10 @@ + SimpleData::from('A')), - dataCollection: Lazy::create(fn () => SimpleData::collect(['B', 'C'], DataCollection::class)), - fakeModel: Lazy::create(fn () => FakeModel::factory()->create([ - 'string' => 'lazy', - ])), - ); - } - - public static function fromString(string $name): self - { - return new self( - id: 1, - simple: SimpleData::from($name), - dataCollection: SimpleData::collect(['B', 'C'], DataCollection::class), - fakeModel: FakeModel::factory()->create([ - 'string' => 'non-lazy', - ]), - ); - } -} diff --git a/tests/PartialsTest.php b/tests/PartialsTest.php index 0e5a973a..4e30375d 100644 --- a/tests/PartialsTest.php +++ b/tests/PartialsTest.php @@ -1,5 +1,7 @@ LazyData::from('Hello')), Lazy::create(fn () => LazyData::collect(['is', 'it', 'me', 'your', 'looking', 'for',])), @@ -97,7 +99,7 @@ public function __construct( ]); }); -it('can include specific nested data', function () { +it('can include specific nested data collections', function () { class TestSpecificDefinedIncludeableCollectedAndNestedLazyData extends Data { public function __construct( @@ -114,43 +116,40 @@ public function __construct( $data = new \TestSpecificDefinedIncludeableCollectedAndNestedLazyData($collection); - expect($data->include('songs.name')->toArray()) - ->toMatchArray([ - 'songs' => [ - ['name' => DummyDto::rick()->name], - ['name' => DummyDto::bon()->name], - ], - ]); + expect($data->include('songs.name')->toArray())->toMatchArray([ + 'songs' => [ + ['name' => DummyDto::rick()->name], + ['name' => DummyDto::bon()->name], + ], + ]); - expect($data->include('songs.{name,artist}')->toArray()) - ->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - ], + expect($data->include('songs.{name,artist}')->toArray())->toMatchArray([ + 'songs' => [ + [ + 'name' => DummyDto::rick()->name, + 'artist' => DummyDto::rick()->artist, ], - ]); + [ + 'name' => DummyDto::bon()->name, + 'artist' => DummyDto::bon()->artist, + ], + ], + ]); - expect($data->include('songs.*')->toArray()) - ->toMatchArray([ - 'songs' => [ - [ - 'name' => DummyDto::rick()->name, - 'artist' => DummyDto::rick()->artist, - 'year' => DummyDto::rick()->year, - ], - [ - 'name' => DummyDto::bon()->name, - 'artist' => DummyDto::bon()->artist, - 'year' => DummyDto::bon()->year, - ], + expect($data->include('songs.*')->toArray())->toMatchArray([ + 'songs' => [ + [ + 'name' => DummyDto::rick()->name, + 'artist' => DummyDto::rick()->artist, + 'year' => DummyDto::rick()->year, ], - ]); + [ + 'name' => DummyDto::bon()->name, + 'artist' => DummyDto::bon()->artist, + 'year' => DummyDto::bon()->year, + ], + ], + ]); }); it('can have a conditional lazy data', function () { @@ -312,7 +311,7 @@ public static function create(string $name): static 'include' => 'name', ])); - expect($response->getData(true))->toBeArray(['name' => 'Ruben']); + expect($response->getData(true))->toMatchArray(['name' => 'Ruben']); LazyData::$allowedIncludes = null; @@ -339,7 +338,7 @@ public static function create(string $name): static expect($excludedResponse->getData(true))->toBe([]); }); -it('can disabled excluding data dynamically from the request', function () { +it('can disable excluding data dynamically from the request', function () { DefaultLazyData::$allowedExcludes = []; $response = DefaultLazyData::from('Ruben')->toResponse(request()->merge([ @@ -365,7 +364,7 @@ public static function create(string $name): static expect($response->getData(true))->toBe([]); }); -it('can disabled only data dynamically from the request', function () { +it('can disable only data dynamically from the request', function () { OnlyData::$allowedOnly = []; $response = OnlyData::from([ @@ -401,7 +400,7 @@ public static function create(string $name): static ]); }); -it('can disabled except data dynamically from the request', function () { +it('can disable except data dynamically from the request', function () { ExceptData::$allowedExcept = []; $response = ExceptData::from(['first_name' => 'Ruben', 'last_name' => 'Van Assche'])->toResponse(request()->merge([ @@ -436,7 +435,7 @@ public static function create(string $name): static it('will not include lazy optional values when transforming', function () { - $data = new class ('Hello World', Lazy::create(fn () => Optional::make())) extends Data { + $data = new class ('Hello World', Lazy::create(fn () => Optional::create())) extends Data { public function __construct( public string $string, public string|Optional|Lazy $lazy_optional_string, @@ -444,7 +443,7 @@ public function __construct( } }; - expect($data->toArray())->toMatchArray([ + expect(($data)->include('lazy_optional_string')->toArray())->toMatchArray([ 'string' => 'Hello World', ]); }); @@ -459,21 +458,6 @@ public function __construct( expect($data->toArray())->toBe([]); }); -it('includes value if not optional data', function () { - $dataClass = new class () extends Data { - public string|Optional $name; - }; - - $data = $dataClass::from([ - 'name' => 'Freek', - ]); - - expect($data->toArray())->toMatchArray([ - 'name' => 'Freek', - ]); -}); - - it('can conditionally include', function () { expect( MultiLazyData::from(DummyDto::rick())->includeWhen('artist', false)->toArray() @@ -933,25 +917,34 @@ public function __construct( }); -it('can fetch lazy union data', function () { - $data = UnionData::from(1); +it('can fetch lazy properties like regular properties within PHP', function () { - expect($data->id)->toBe(1); - expect($data->simple->string)->toBe('A'); - expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); - expect($data->fakeModel->string)->toBe('lazy'); -}); + $dataClass = new class extends Data { + public int $id; -it('can fetch non-lazy union data', function () { - $data = UnionData::from('A'); + public SimpleData|Lazy $simple; - expect($data->id)->toBe(1); + #[DataCollectionOf(SimpleData::class)] + public DataCollection|Lazy $dataCollection; + + public FakeModel|Lazy $fakeModel; + }; + + $data = $dataClass::from([ + 'id' => 42, + 'simple' => Lazy::create(fn () => SimpleData::from('A')), + 'dataCollection' => Lazy::create(fn () => SimpleData::collect(['B', 'C'], DataCollection::class)), + 'fakeModel' => Lazy::create(fn () => FakeModel::factory()->create([ + 'string' => 'lazy', + ])), + ]); + + expect($data->id)->toBe(42); expect($data->simple->string)->toBe('A'); expect($data->dataCollection->toCollection()->pluck('string')->toArray())->toBe(['B', 'C']); - expect($data->fakeModel->string)->toBe('non-lazy'); + expect($data->fakeModel->string)->toBe('lazy'); }); - it('has array access and will replicate partialtrees (collection)', function () { $collection = MultiData::collect([ new MultiData('first', 'second'), @@ -986,7 +979,7 @@ public function __construct( ]); }); -it('can disabled manually including data in the request (collection)', function () { +it('can disable manually including data in the request (collection)', function () { LazyData::$allowedIncludes = []; $response = (new DataCollection(LazyData::class, ['Ruben', 'Freek', 'Brent']))->toResponse(request()->merge([ @@ -1093,3 +1086,107 @@ public function __construct( [], ]); }); + +it('can work with the different types of lazy data collections', function ( + Data $dataClass, + Closure $itemsClosure +) { + $data = $dataClass::from([ + 'lazyCollection' => $itemsClosure([ + SimpleData::from('A'), + SimpleData::from('B'), + ]), + + 'nestedLazyCollection' => $itemsClosure([ + NestedLazyData::from('C'), + NestedLazyData::from('D'), + ]), + ]); + + expect($data->toArray())->toMatchArray([]); + + expect($data->include('lazyCollection')->toArray())->toMatchArray([ + 'lazyCollection' => [ + ['string' => 'A'], + ['string' => 'B'], + ], + + 'nestedLazyCollection' => [ + [], + [], + ], + ]); + + expect($data->include('lazyCollection', 'nestedLazyCollection.simple')->toArray())->toMatchArray([ + 'lazyCollection' => [ + ['string' => 'A'], + ['string' => 'B'], + ], + + 'nestedLazyCollection' => [ + ['simple' => ['string' => 'C']], + ['simple' => ['string' => 'D']], + ], + ]); +})->with(function () { + yield 'array' => [ + fn () => new class extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|array $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|array $nestedLazyCollection; + }, + fn () => fn (array $items) => $items, + ]; + + yield 'collection' => [ + fn () => new class extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|Collection $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|Collection $nestedLazyCollection; + }, + fn () => fn (array $items) => $items, + ]; + + yield 'paginator' => [ + fn () => new class extends Data { + #[DataCollectionOf(SimpleData::class)] + public Lazy|LengthAwarePaginator $lazyCollection; + + #[DataCollectionOf(NestedLazyData::class)] + public Lazy|LengthAwarePaginator $nestedLazyCollection; + }, + fn () => fn (array $items) => new LengthAwarePaginator($items, count($items), 15), + ]; +})->skip('Impelemnt further'); + +it('partials are always reset when transforming again', function () { + $dataClass = new class(Lazy::create(fn () => NestedLazyData::from('Hello World'))) extends Data { + public function __construct( + public Lazy|NestedLazyData $nested + ) { + } + }; + + dd($dataClass->include('nested')->exclude()->toArray()); + // ['nested' => ['simple' => ['string' => 'Hello World']],] + $dataClass->toArray(); + // ['nested' => ['simple' => ['string' => 'Hello World']],] + + expect($dataClass->include('nested.simple')->toArray())->toBe([ + 'nested' => ['simple' => ['string' => 'Hello World']], + ]); + + expect($dataClass->include('nested')->toArray())->toBe([ + 'nested' => [], + ]); + + expect($dataClass->include()->toArray())->toBeEmpty(); +})->skip('Add a reset partials method'); + +it('can set partials on a nested data object and these will be respected', function () { + +})->skip('Impelemnt');