diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5e69dbb..7c6c905fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,26 @@ All notable changes to `laravel-data` will be documented in this file. - Allow creating data objects using `from` without parameters - Add support for a Dto and Resource object +## 3.10.0 - 2023-12-01 + +A fresh release after a month of vacation, enjoy! + +- Add ExcludeWith validation rule (#584) +- Add DoesntEndWith and DoesntStartWith validation rules (#585) +- Add MinDigits and MaxDigits validation rules (#586) +- Add DataCollection reject() method (#593) +- Add generic type for WithData trait (#597) +- Fix issue where non set optional values were transformed (#602) +- Fix issue where parameters passed to Laravel Collection methods would sometimes provide alternative results (issue: #607) + +## 1.5.3 - 2023-12-01 + +- MimeTypes validation fix on v1 (#596) + +## 3.9.2 - 2023-10-20 + +- Fix breaking compatibility #590 + ## 3.9.1 - 2023-10-12 - Add Declined and DeclinedIf validation attributes (#572) @@ -44,7 +64,7 @@ All notable changes to `laravel-data` will be documented in this file. - fix target namespace when creating files with Laravel Idea (#497) - allow collection to be created passing null (#507) -- add Ulid validation rule (#510) +- add Ulid validation rule (#510) -add TARGET_PARAMETER to Attribute for improved Validation (#523) ## 3.7.0 - 2023-07-05 diff --git a/docs/advanced-usage/validation-attributes.md b/docs/advanced-usage/validation-attributes.md index 968f4aa33..58eb28caf 100644 --- a/docs/advanced-usage/validation-attributes.md +++ b/docs/advanced-usage/validation-attributes.md @@ -288,6 +288,36 @@ public string $closure; public string $closure; ``` +## DoesntEndWith + +[Docs](https://laravel.com/docs/9.x/validation#rule-doesnt-end-with) + +```php +#[DoesntEndWith('a')] +public string $closure; + +#[DoesntEndWith(['a', 'b'])] +public string $closure; + +#[DoesntEndWith('a', 'b')] +public string $closure; +``` + +## DoesntStartWith + +[Docs](https://laravel.com/docs/9.x/validation#rule-doesnt-start-with) + +```php +#[DoesntStartWith('a')] +public string $closure; + +#[DoesntStartWith(['a', 'b'])] +public string $closure; + +#[DoesntStartWith('a', 'b')] +public string $closure; +``` + ## Email [Docs](https://laravel.com/docs/9.x/validation#rule-email) @@ -352,6 +382,17 @@ public string $closure; public string $closure; ``` +## ExcludeWith + +*At the moment the data is not yet excluded due to technical reasons, v4 should fix this* + +[Docs](https://laravel.com/docs/9.x/validation#rule-exclude-with) + +```php +#[ExcludeWith('other_field')] +public string $closure; +``` + ## ExcludeWithout *At the moment the data is not yet excluded due to technical reasons, v4 should fix this* @@ -513,6 +554,15 @@ public int $closure; public int $closure; ``` +## Lowercase + +[Docs](https://laravel.com/docs/9.x/validation#rule-lowercase) + +```php +#[Lowercase] +public string $closure; +``` + ## MacAddress [Docs](https://laravel.com/docs/9.x/validation#rule-mac) @@ -531,6 +581,15 @@ public string $closure; public int $closure; ``` +## MaxDigits + +[Docs](https://laravel.com/docs/9.x/validation#rule-max-digits) + +```php +#[MaxDigits(10)] +public int $closure; +``` + ## MimeTypes [Docs](https://laravel.com/docs/9.x/validation#rule-mimetypes) @@ -570,6 +629,15 @@ public UploadedFile $closure; public int $closure; ``` +## MinDigits + +[Docs](https://laravel.com/docs/9.x/validation#rule-min-digits) + +```php +#[MinDigits(2)] +public int $closure; +``` + ## MultipleOf [Docs](https://laravel.com/docs/9.x/validation#rule-multiple-of) @@ -895,6 +963,15 @@ public string $closure; public string $closure; ``` +## Uppercase + +[Docs](https://laravel.com/docs/9.x/validation#rule-uppercase) + +```php +#[Uppercase] +public string $closure; +``` + ## Url [Docs](https://laravel.com/docs/9.x/validation#rule-url) diff --git a/src/Attributes/Validation/DoesntEndWith.php b/src/Attributes/Validation/DoesntEndWith.php new file mode 100644 index 000000000..896c486dd --- /dev/null +++ b/src/Attributes/Validation/DoesntEndWith.php @@ -0,0 +1,28 @@ +values = Arr::flatten($values); + } + + public static function keyword(): string + { + return 'doesnt_end_with'; + } + + public function parameters(): array + { + return [$this->values]; + } +} diff --git a/src/Attributes/Validation/DoesntStartWith.php b/src/Attributes/Validation/DoesntStartWith.php new file mode 100644 index 000000000..b638452b7 --- /dev/null +++ b/src/Attributes/Validation/DoesntStartWith.php @@ -0,0 +1,28 @@ +values = Arr::flatten($values); + } + + public static function keyword(): string + { + return 'doesnt_start_with'; + } + + public function parameters(): array + { + return [$this->values]; + } +} diff --git a/src/Attributes/Validation/ExcludeWith.php b/src/Attributes/Validation/ExcludeWith.php new file mode 100644 index 000000000..68e2a1a14 --- /dev/null +++ b/src/Attributes/Validation/ExcludeWith.php @@ -0,0 +1,28 @@ +field = $this->parseFieldReference($field); + } + + public static function keyword(): string + { + return 'exclude_with'; + } + + public function parameters(): array + { + return [$this->field]; + } +} diff --git a/src/Attributes/Validation/Lowercase.php b/src/Attributes/Validation/Lowercase.php new file mode 100644 index 000000000..71ead99fd --- /dev/null +++ b/src/Attributes/Validation/Lowercase.php @@ -0,0 +1,19 @@ +value]; + } +} diff --git a/src/Attributes/Validation/MinDigits.php b/src/Attributes/Validation/MinDigits.php new file mode 100644 index 000000000..e71b3b33c --- /dev/null +++ b/src/Attributes/Validation/MinDigits.php @@ -0,0 +1,24 @@ +value]; + } +} diff --git a/src/Attributes/Validation/Uppercase.php b/src/Attributes/Validation/Uppercase.php new file mode 100644 index 000000000..5d69acab3 --- /dev/null +++ b/src/Attributes/Validation/Uppercase.php @@ -0,0 +1,19 @@ +items = $cloned->items->map($through); + $cloned->items = $cloned->items->map(...func_get_args()); return $cloned; } @@ -35,7 +35,7 @@ public function through(callable $through): static */ public function map(callable $map): static { - return $this->through($map); + return $this->through(...func_get_args()); } /** @@ -47,7 +47,21 @@ public function filter(callable $filter): static { $cloned = clone $this; - $cloned->items = $cloned->items->filter($filter); + $cloned->items = $cloned->items->filter(...func_get_args()); + + return $cloned; + } + + /** + * @param callable(TValue): bool $filter + * + * @return static + */ + public function reject(callable $filter): static + { + $cloned = clone $this; + + $cloned->items = $cloned->items->reject(...func_get_args()); return $cloned; } @@ -62,7 +76,7 @@ public function filter(callable $filter): static */ public function first(callable|null $callback = null, $default = null) { - return $this->items->first($callback, $default); + return $this->items->first(...func_get_args()); } /** @@ -75,7 +89,7 @@ public function first(callable|null $callback = null, $default = null) */ public function last(callable|null $callback = null, $default = null) { - return $this->items->last($callback, $default); + return $this->items->last(...func_get_args()); } /** @@ -85,7 +99,7 @@ public function last(callable|null $callback = null, $default = null) */ public function each(callable $callback): static { - $this->items->each($callback); + $this->items->each(...func_get_args()); return $this; } @@ -106,7 +120,7 @@ public function where(string $key, mixed $operator = null, mixed $value = null): { $cloned = clone $this; - $cloned->items = $cloned->items->where($key, $operator, $value); + $cloned->items = $cloned->items->where(...func_get_args()); return $cloned; } @@ -122,7 +136,7 @@ public function where(string $key, mixed $operator = null, mixed $value = null): */ public function reduce(callable $callback, mixed $initial = null) { - return $this->items->reduce($callback, $initial); + return $this->items->reduce(...func_get_args()); } /** diff --git a/src/Support/Validation/ValidationRuleFactory.php b/src/Support/Validation/ValidationRuleFactory.php index 4bc1f5b6e..6e52c93c6 100644 --- a/src/Support/Validation/ValidationRuleFactory.php +++ b/src/Support/Validation/ValidationRuleFactory.php @@ -30,11 +30,14 @@ use Spatie\LaravelData\Attributes\Validation\DigitsBetween; use Spatie\LaravelData\Attributes\Validation\Dimensions; use Spatie\LaravelData\Attributes\Validation\Distinct; +use Spatie\LaravelData\Attributes\Validation\DoesntEndWith; +use Spatie\LaravelData\Attributes\Validation\DoesntStartWith; use Spatie\LaravelData\Attributes\Validation\Email; use Spatie\LaravelData\Attributes\Validation\EndsWith; use Spatie\LaravelData\Attributes\Validation\Enum; use Spatie\LaravelData\Attributes\Validation\ExcludeIf; use Spatie\LaravelData\Attributes\Validation\ExcludeUnless; +use Spatie\LaravelData\Attributes\Validation\ExcludeWith; use Spatie\LaravelData\Attributes\Validation\ExcludeWithout; use Spatie\LaravelData\Attributes\Validation\Exists; use Spatie\LaravelData\Attributes\Validation\File; @@ -51,11 +54,14 @@ use Spatie\LaravelData\Attributes\Validation\Json; use Spatie\LaravelData\Attributes\Validation\LessThan; use Spatie\LaravelData\Attributes\Validation\LessThanOrEqualTo; +use Spatie\LaravelData\Attributes\Validation\Lowercase; use Spatie\LaravelData\Attributes\Validation\MacAddress; use Spatie\LaravelData\Attributes\Validation\Max; +use Spatie\LaravelData\Attributes\Validation\MaxDigits; use Spatie\LaravelData\Attributes\Validation\Mimes; use Spatie\LaravelData\Attributes\Validation\MimeTypes; use Spatie\LaravelData\Attributes\Validation\Min; +use Spatie\LaravelData\Attributes\Validation\MinDigits; use Spatie\LaravelData\Attributes\Validation\MultipleOf; use Spatie\LaravelData\Attributes\Validation\NotIn; use Spatie\LaravelData\Attributes\Validation\NotRegex; @@ -84,6 +90,7 @@ use Spatie\LaravelData\Attributes\Validation\Timezone; use Spatie\LaravelData\Attributes\Validation\Ulid; use Spatie\LaravelData\Attributes\Validation\Unique; +use Spatie\LaravelData\Attributes\Validation\Uppercase; use Spatie\LaravelData\Attributes\Validation\Url; use Spatie\LaravelData\Attributes\Validation\Uuid; use Spatie\LaravelData\Exceptions\CouldNotCreateValidationRule; @@ -134,10 +141,13 @@ protected function mapping(): array Dimensions::keyword() => Dimensions::class, Distinct::keyword() => Distinct::class, Email::keyword() => Email::class, + DoesntEndWith::keyword() => DoesntEndWith::class, + DoesntStartWith::keyword() => DoesntStartWith::class, EndsWith::keyword() => EndsWith::class, Enum::keyword() => Enum::class, ExcludeIf::keyword() => ExcludeIf::class, ExcludeUnless::keyword() => ExcludeUnless::class, + ExcludeWith::keyword() => ExcludeWith::class, ExcludeWithout::keyword() => ExcludeWithout::class, Exists::keyword() => Exists::class, File::keyword() => File::class, @@ -154,11 +164,14 @@ protected function mapping(): array Json::keyword() => Json::class, LessThan::keyword() => LessThan::class, LessThanOrEqualTo::keyword() => LessThanOrEqualTo::class, + Lowercase::keyword() => Lowercase::class, MacAddress::keyword() => MacAddress::class, Max::keyword() => Max::class, + MaxDigits::keyword() => MaxDigits::class, Mimes::keyword() => Mimes::class, MimeTypes::keyword() => MimeTypes::class, Min::keyword() => Min::class, + MinDigits::keyword() => MinDigits::class, MultipleOf::keyword() => MultipleOf::class, NotIn::keyword() => NotIn::class, NotRegex::keyword() => NotRegex::class, @@ -186,6 +199,7 @@ protected function mapping(): array StringType::keyword() => StringType::class, Timezone::keyword() => Timezone::class, Unique::keyword() => Unique::class, + Uppercase::keyword() => Uppercase::class, Url::keyword() => Url::class, Ulid::keyword() => Ulid::class, Uuid::keyword() => Uuid::class, diff --git a/src/Transformers/DataTransformer.php b/src/Transformers/DataTransformer.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/WithData.php b/src/WithData.php index 334ee2c96..9b4477924 100644 --- a/src/WithData.php +++ b/src/WithData.php @@ -3,12 +3,17 @@ namespace Spatie\LaravelData; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\Contracts\DataObject; use Spatie\LaravelData\Exceptions\InvalidDataClass; +/** + * @template T + */ trait WithData { - public function getData(): DataObject + /** + * @return T + */ + public function getData() { $dataClass = match (true) { /** @psalm-suppress UndefinedThisPropertyFetch */ diff --git a/tests/DataCollectionTest.php b/tests/DataCollectionTest.php index 42f551c3d..4f807860a 100644 --- a/tests/DataCollectionTest.php +++ b/tests/DataCollectionTest.php @@ -79,6 +79,17 @@ ->toMatchArray($filtered); }); +test('a collection can be rejected', function () { + $collection = SimpleData::collection(['A', 'B']); + + $filtered = $collection->reject(fn (SimpleData $data) => $data->string === 'B')->toArray(); + + expect([ + ['string' => 'A'], + ]) + ->toMatchArray($filtered); +}); + test('a collection can be transformed', function () { $collection = new DataCollection(SimpleData::class, ['A', 'B']); diff --git a/tests/Datasets/Attributes/RulesDataset.php b/tests/Datasets/Attributes/RulesDataset.php index ef09fa9ef..fdc878dd7 100644 --- a/tests/Datasets/Attributes/RulesDataset.php +++ b/tests/Datasets/Attributes/RulesDataset.php @@ -33,11 +33,14 @@ use Spatie\LaravelData\Attributes\Validation\DigitsBetween; use Spatie\LaravelData\Attributes\Validation\Dimensions; use Spatie\LaravelData\Attributes\Validation\Distinct; +use Spatie\LaravelData\Attributes\Validation\DoesntEndWith; +use Spatie\LaravelData\Attributes\Validation\DoesntStartWith; use Spatie\LaravelData\Attributes\Validation\Email; use Spatie\LaravelData\Attributes\Validation\EndsWith; use Spatie\LaravelData\Attributes\Validation\Enum; use Spatie\LaravelData\Attributes\Validation\ExcludeIf; use Spatie\LaravelData\Attributes\Validation\ExcludeUnless; +use Spatie\LaravelData\Attributes\Validation\ExcludeWith; use Spatie\LaravelData\Attributes\Validation\ExcludeWithout; use Spatie\LaravelData\Attributes\Validation\Exists; use Spatie\LaravelData\Attributes\Validation\File; @@ -54,11 +57,14 @@ use Spatie\LaravelData\Attributes\Validation\Json; use Spatie\LaravelData\Attributes\Validation\LessThan; use Spatie\LaravelData\Attributes\Validation\LessThanOrEqualTo; +use Spatie\LaravelData\Attributes\Validation\Lowercase; use Spatie\LaravelData\Attributes\Validation\MacAddress; use Spatie\LaravelData\Attributes\Validation\Max; +use Spatie\LaravelData\Attributes\Validation\MaxDigits; use Spatie\LaravelData\Attributes\Validation\Mimes; use Spatie\LaravelData\Attributes\Validation\MimeTypes; use Spatie\LaravelData\Attributes\Validation\Min; +use Spatie\LaravelData\Attributes\Validation\MinDigits; use Spatie\LaravelData\Attributes\Validation\MultipleOf; use Spatie\LaravelData\Attributes\Validation\NotIn; use Spatie\LaravelData\Attributes\Validation\NotRegex; @@ -87,6 +93,7 @@ use Spatie\LaravelData\Attributes\Validation\Timezone; use Spatie\LaravelData\Attributes\Validation\Ulid; use Spatie\LaravelData\Attributes\Validation\Unique; +use Spatie\LaravelData\Attributes\Validation\Uppercase; use Spatie\LaravelData\Attributes\Validation\Url; use Spatie\LaravelData\Attributes\Validation\Uuid; use Spatie\LaravelData\Exceptions\CannotBuildValidationRule; @@ -119,6 +126,8 @@ function fixature( yield from dateEqualsAttributes(); yield from dimensionsAttributes(); yield from distinctAttributes(); + yield from doesntEndWithAttributes(); + yield from doesntStartWithAttributes(); yield from declinedIfAttributes(); yield from emailAttributes(); yield from endsWithAttributes(); @@ -230,6 +239,11 @@ function fixature( expected: 'exclude_unless:field,42', ); + yield fixature( + attribute: new ExcludeWith('field'), + expected: 'exclude_with:field', + ); + yield fixature( attribute: new ExcludeWithout('field'), expected: 'exclude_without:field', @@ -300,6 +314,11 @@ function fixature( expected: 'lte:field', ); + yield fixature( + attribute: new Lowercase(), + expected: 'lowercase', + ); + yield fixature( attribute: new MacAddress(), expected: 'mac_address', @@ -310,11 +329,21 @@ function fixature( expected: 'max:10', ); + yield fixature( + attribute: new MaxDigits(10), + expected: 'max_digits:10', + ); + yield fixature( attribute: new Min(10), expected: 'min:10', ); + yield fixature( + attribute: new MinDigits(10), + expected: 'min_digits:10', + ); + yield fixature( attribute: new MultipleOf(10), expected: 'multiple_of:10', @@ -375,6 +404,11 @@ function fixature( expected: 'timezone', ); + yield fixature( + attribute: new Uppercase(), + expected: 'uppercase', + ); + yield fixature( attribute: new Url(), expected: 'url', @@ -603,6 +637,42 @@ function distinctAttributes(): Generator ); } +function doesntEndWithAttributes(): Generator +{ + yield fixature( + attribute: new DoesntEndWith('x'), + expected: 'doesnt_end_with:x', + ); + + yield fixature( + attribute: new DoesntEndWith(['x', 'y']), + expected: 'doesnt_end_with:x,y', + ); + + yield fixature( + attribute: new DoesntEndWith('x', 'y'), + expected: 'doesnt_end_with:x,y', + ); +} + +function doesntStartWithAttributes(): Generator +{ + yield fixature( + attribute: new DoesntStartWith('x'), + expected: 'doesnt_start_with:x', + ); + + yield fixature( + attribute: new DoesntStartWith(['x', 'y']), + expected: 'doesnt_start_with:x,y', + ); + + yield fixature( + attribute: new DoesntStartWith('x', 'y'), + expected: 'doesnt_start_with:x,y', + ); +} + function declinedIfAttributes(): Generator { yield fixature( diff --git a/tests/ValidationTest.php b/tests/ValidationTest.php index 4a7d8cf49..20180e222 100644 --- a/tests/ValidationTest.php +++ b/tests/ValidationTest.php @@ -2380,12 +2380,14 @@ public static function rules(ValidationContext $context): array ->assertErrors(['success' => true]); })->skip('V4: The rule inferrers need to be rewritten/removed for this, we need to first add attribute rules and then decide require stuff'); -it('will not transform non set optional properties ', function () { +it('can validate an optional but nonexists attribute', function () { $dataClass = new class () extends Data { - public array|Optional $optional; + public array|null|Optional $property; }; - expect($dataClass::from([])->toArray())->toBeEmpty(); - expect($dataClass::from()->toArray())->toBeEmpty(); - expect((new ($dataClass::class))->toArray())->toBeEmpty(); + expect($dataClass::from()->toArray())->toBe([]); + expect($dataClass::from([])->toArray())->toBe([]); + expect($dataClass::from(['property' => null])->toArray())->toBe(['property' => null]); + expect($dataClass::from(['property' => []])->toArray())->toBe(['property' => []]); + expect($dataClass::validateAndCreate([])->toArray())->toBe([]); });